API reference¶
Generated from the source, so it cannot drift from the code.
Session¶
wintegrate.session.Session ¶
Orchestrates test execution environment, continuous video recording, pre/post window census diffing, and automatic failure artifact generation. Supports dynamic Windows 11 Virtual Desktop isolation.
log_event ¶
Records a structured event, in memory and -- once open -- in the journal.
Every line carries the wall clock, a monotonic offset, the pid and the current step. The pid is not decoration: a console is shared, orphans outlive their parent, and a journal opened in append mode can hold more than one process's lines. Without it nothing says who wrote what.
step ¶
Names a block of work, so the artifacts say which step failed.
with session.step("fill in the form"):
form.type_verified("...")
with session.step("submit"):
submit.invoke()
On a machine you cannot connect to, "which step" is more useful than "which line". This records the step's start, duration and outcome in the event timeline, captures a screenshot named after the step when the block raises, and prefixes the exception message with the step name so the pytest one-liner already tells you where you were.
The original exception type is preserved and re-raised: this adds context, it does not swallow or convert failures.
capture_screenshot ¶
Saves a screenshot into the session's artifact directory and returns its path.
Use this to record what the screen looked like at a point you care about — the failure screenshot only exists when something raised, and by then the state that explains it may be gone.
Pass window to capture that window alone, including anything covering it.
Otherwise the whole virtual desktop is captured; all_monitors=False
narrows that to the primary display.
find_window ¶
find_window(title_exact: str | None = None, title_pattern: str | None = None, class_name: str | None = None, timeout: float | None = None) -> Window
Finds an existing top-level window matching criteria.
launch_and_discover ¶
launch_and_discover(cmd: list[str] | str, timeout: float | None = None, title_pattern: str | None = None, exclude_hwnds: set[int] | None = None, process_names: tuple[str, ...] | list[str] | None = None, window_classes: tuple[str, ...] | list[str] | None = None) -> tuple[subprocess.Popen, Window]
Wrapper around Window.launch_and_discover with session logging.
app ¶
app(spec: AppSpec | list[str] | str, timeout: float | None = None, fresh: bool | str = 'auto', title_pattern: str | None = None, exclude_hwnds: set[int] | None = None) -> AppHandle
Launches an application with a managed lifecycle. Use as a context manager:
with session.app(NOTEPAD) as app:
editor = app.find_text_input()
editor.type_verified("hello\n", expected_line_count_delta=1)
- Matching prefers locale-independent identities from the AppSpec (process image names, window classes) over window titles.
- fresh="auto" (CI only) sweeps leftover instances of the app before launching: Store apps are single-instance, so a leaked instance makes the next launch open a tab instead of a new window. Pass fresh=False when launching a second instance of the same app deliberately.
- The default timeout is generous because cold Store-app starts on CI runners regularly exceed 10s.
- Cleanup (window close + process kill) runs on context exit even when the body raises.
wintegrate.session.SessionConfig
dataclass
¶
Applications¶
wintegrate.apps.AppSpec
dataclass
¶
Locale-independent identity of an application for launch-and-discover.
wintegrate.apps.AppHandle ¶
Owns one launched application: window + launcher process + guaranteed cleanup.
locator ¶
Returns a Playwright-style Locator rooted at this application's main window.
get_by_role ¶
Finds elements by role in this application's main window.
get_by_text ¶
Finds elements by text in this application's main window.
get_by_automation_id ¶
Finds elements by automation_id in this application's main window.
get_by_class ¶
Finds elements by class name in this application's main window.
close ¶
Closes the window, forcing it only when the process is not one we need.
Window.close(force=True) ends the window's process, and this runs on
every AppHandle -- including one built around a Window that came from
Window.find() rather than from a launch here. That window can belong to
the caller, or to the shell, or to the terminal hosting this process's
console; ending the last of those ends this process, and on CI the agent
reporting the job with it.
The check is by measured relation rather than by name, and it downgrades rather than refusing: WM_CLOSE is still sent, which is the right request for a window this code did not open.
Windows¶
wintegrate.window.Window ¶
Represents a top-level native OS window.
is_visible
property
¶
IsWindowVisible, which is not the same as "on screen".
A window DWM has cloaked — a WinUI or UWP app that has hidden itself, or
anything on another virtual desktop — still answers True here. Use
is_on_screen when the question is whether a user could see it.
cloak_reason
property
¶
Why DWM is hiding this window; CloakReason(0) if it is not.
None means the attribute could not be read at all, which is not the same
answer as "not cloaked" — see get_window_cloak_reason.
is_cloaked
property
¶
Whether DWM is hiding this window. None when it cannot be determined.
display_affinity
property
¶
Whether this window has asked to be kept out of screen captures.
None means it could not be read, which is not the same answer as
DisplayAffinity.NONE — see get_window_display_affinity.
is_excluded_from_capture
property
¶
Whether captures of this window will come back without it in them.
None when it cannot be determined. Worth checking before trusting a
screenshot or a recording of a specific application: this is the one
reason for a missing window that is_visible, cloak_reason and
is_on_screen all answer wrongly, because the window really is visible
and really is on screen — only capture cannot see it.
is_on_screen
property
¶
Whether this window is somewhere a user could actually see it.
IsWindowVisible and not cloaked. The second half is the part that gets
left out: a Command Palette that has dismissed itself, a Store app that
has been put away, and a window on another virtual desktop all report
is_visible == True, so a test waiting for one of them to disappear waits
forever and a test asserting one is gone passes while it is still there.
A window whose cloak state cannot be read falls back to is_visible —
that is the best available answer, not a silent False.
keyboard_layout
property
¶
The active keyboard layout (HKL) of the thread that owns this window.
keyboard_language_id
property
¶
The LANGID (low word of the HKL) of this window's keyboard layout.
ime_mode ¶
Puts this window's IME into a known conversion mode for the block.
with dialog.ime_mode(ImeConversion.ALPHANUMERIC):
edit.send_physical_keys("hello")
Why this exists: a scan code means whatever the active input state says it means. Under Bopomofo in native mode, unshifted letters are phonetic keys the IME swallows into composition, so correct scan-code injection produces an empty field. Establishing the mode is the only way to make that deterministic — detecting it is not possible (see get_ime_status).
On exit the previous mode is restored only if it could be read. A None reading means no IME window answered, which is not the same as alphanumeric; restoring a guess would leave the desktop in a state the caller never asked for.
foreground ¶
Brings this window to the foreground for the block, then gives it back.
with dialog.foreground():
edit.send_physical_keys("hello")
The foreground window is a global, shared resource: a test that takes it and never returns it leaves the next one typing into whatever it grabbed. Restoration is best-effort — the previous window may be gone by now, and failing teardown over that would mask the real result.
get_ime_status ¶
Reads this window's IME state: has_context, is_open, conversion,
sentence, native_mode, full_shape.
has_context is False for a modern XAML/WinUI control, which routes text
services through TSF rather than IMM32 — that means "ask TSF", not "IME off".
set_ime_open ¶
Opens or closes this window's IME. False when it has no IMM32 context.
set_ime_conversion ¶
Sets this window's IME conversion mode (see the IME_CMODE_* flags).
get_composition_string ¶
Returns the IME's in-progress composition text for this window ("" when idle).
set_keyboard_layout_verified ¶
Switches this window's thread to a keyboard layout and confirms it took.
layout_id is the eight-hex-digit identifier — "00000409" for en-US,
"00000404" for zh-TW. Returns the resulting HKL.
Necessary because the active layout changes what a keystroke means: with a Bopomofo layout, unshifted letters are phonetic keys that the IME swallows into composition, so scan-code input that is completely correct produces an empty field. A test that assumes the machine's layout passes or fails on where it happens to run.
The switch is requested, not commanded: only the owning thread can change its own layout, so this posts WM_INPUTLANGCHANGEREQUEST and then verifies.
It can legitimately fail. A layout is loaded per process, and a window in another process will reject a request naming an HKL it has never loaded — measured on a zh-TW Windows 11 ARM64 desktop, where every variant of the request (post and send, with SYSCHARSET, FORWARD and no flag) left the target thread on 0x04040404. Callers that need a known layout should treat ActionVerificationError as "this machine cannot give me one" and skip, rather than continuing with input that will mean something else.
capture ¶
Captures just this window, including anything covering it, and returns a
PIL Image. Saves to path when given.
Prefer this over a full screenshot when diagnosing a specific window: the thing that broke the run is often the popup on top of it, and a desktop capture shows that instead of the window you were driving.
exists ¶
Returns whether this window handle is still a live window.
require_visible is IsWindowVisible, which a cloaked window passes —
so for a WinUI or UWP window that hides itself, this returns True after it
has gone away. Pass require_on_screen=True when the question is whether
it is somewhere a user could see it; see is_on_screen.
The default is unchanged deliberately: quietly tightening it would change what every existing caller measures.
re_resolve_element ¶
Always resolves a fresh UIA element directly from the HWND.
find_text_input ¶
find_text_input(timeout: float = 20.0, ladder: tuple[dict, ...] | list[dict] | None = None) -> UiaElement
Locates this window's primary text-input element using a locale-independent ladder of window classes and UIA control types (see DEFAULT_TEXT_INPUT_LADDER).
get_by_role ¶
Finds descendant elements in this window by role (e.g. 'button', 'checkbox', 'tab', 'menuitem').
get_by_text ¶
Finds descendant elements in this window matching text.
get_by_automation_id ¶
Finds descendant elements in this window by UIA automation_id.
get_by_class ¶
Finds descendant elements in this window by window class name.
find_button ¶
find_button(automation_id: str | None = None, name: str | None = None, timeout: float = 5.0) -> UiaElement
Locates a button in this window by automation_id or name.
find_checkbox ¶
Locates a CheckBox in this window.
find_tab_control ¶
Locates a TabControl in this window.
find_menu ¶
Locates a Menu / MenuBar in this window.
find_combobox ¶
Locates a ComboBox in this window.
focus_content_island ¶
Puts keyboard focus inside a WinUI 3 / Windows App SDK content island.
A freshly launched WinUI 3 window can be the foreground window with UIA
focus still on the top-level HWND rather than on anything in the XAML
tree. Every check passes — GetForegroundWindow() returns the window and
set_foreground() reports success — and XAML accelerators are still
dropped, because the content island never sees them. Measured against
Files 4.2.9: Ctrl+T did nothing until focus moved into the island.
Returns True when focus is inside the island (including when it already was), False when the island cannot be found or refuses focus. Nothing is clicked, so no selection or activation happens as a side effect.
Two routes that look like they should work and do not:
SetFocus()on the top-level window's own UIA element leaves focus exactly where it was.- focusing the first keyboard-focusable descendant lands on the caption's
InputNonClientPointerSourceinput sink, which takes focus off the top-level window without giving it to the island — so the accelerator is still dropped, and the obvious "did focus move?" check reports success.
ensure_onscreen ¶
Moves the window into the virtual screen if it is positioned outside it.
A window can be fully off-screen and behave normally in every way that
does not involve a pointer: it is visible, it is foreground, its UIA tree
resolves, and patterns like SelectionItem work. What stops working is
anything that clicks — a synthesised click at a negative coordinate lands
nowhere, click() returns without complaint, and the test fails on the
post-condition with no hint that the cursor never reached the control.
Measured against DB Browser for SQLite, which restores its last window
position: an element reported (-701, -525, -494, -469), and every
button click was silently discarded while tab selection kept working.
Applications that remember their geometry will restore a position saved on a machine with a different screen layout, which is why this is worth checking rather than assuming.
Returns True if the window is on-screen afterwards (including when it already was), False if it could not be moved.
move_to_current_desktop ¶
Moves this window to the currently active virtual desktop if pyvda is available.
set_foreground ¶
Brings the window to the foreground cleanly using Win32 AttachThreadInput. Synchronizes thread input queues to reliably grant foreground activation permission.
maximize ¶
Maximises this window, and confirms it happened.
Sends the window WM_SYSCOMMAND/SC_MAXIMIZE — the message its own
title-bar button sends — rather than calling ShowWindow. Both work on
an ordinary Win32 window, but the message goes through the window's own
handling, which is where a framework that manages its own layout is
listening.
verify polls IsZoomed afterwards rather than trusting the call. A
window can decline: some applications restore a remembered size, and a
window whose default rectangle is already larger than the screen looks
maximised without being maximised. Guessing from a screenshot is how
that gets missed.
Returns whether the window ended up maximised, so a caller who does not care can carry on and one who does can say so.
move_and_resize ¶
Reposition window on screen.
close ¶
Closes window gracefully via WM_CLOSE or forces termination.
find
classmethod
¶
find(title_exact: str | None = None, title_pattern: str | None = None, class_name: str | None = None, pid: int | None = None, timeout: float = 5.0) -> Window
Finds an existing visible top-level window matching ALL supplied criteria.
Criteria are combined with AND: find(title_pattern="Settings",
class_name="#32770") returns the dialog whose title matches, not merely the
first #32770 on the desktop. Pass pid to restrict the search to one
process — the reliable way to target a window whose title is localized,
empty, or duplicated across applications.
launch_and_discover
classmethod
¶
launch_and_discover(cmd: list[str] | str, timeout: float = 10.0, title_pattern: str | None = None, exclude_hwnds: set[int] | None = None, process_names: tuple[str, ...] | list[str] | None = None, window_classes: tuple[str, ...] | list[str] | None = None, require_all: bool = False) -> tuple[subprocess.Popen, Window]
Launches an application and discovers its top-level window by diffing pre/post snapshots. Solves launcher PID != window PID issue and allows excluding existing HWNDs.
Matching prefers locale-independent identities: window_classes (window class
names) and process_names (executable basenames of the window's owning
process). title_pattern remains as a last-resort fallback; a candidate
window is accepted when ANY provided criterion matches.
require_all=True demands that every criterion given match the same window,
which is how you reject a dialog the app puts up instead of its window.
process_names alone cannot: an app's own error dialog runs in the app's
process, so the process name matches and a #32770 is handed back as the
app. Measured against Files 4.2.9 with its .NET runtime missing — discovery
returned a dialog in 1.2s where the real window takes ~19s, and every
element lookup afterwards failed against a window that looked fine.
The child's stdin is DEVNULL and its stdout/stderr never inherit this
process's: inside a Session they go to launched_NN.out / .err in the
artifact directory (and a discovery timeout quotes them), outside one to
DEVNULL. Inherited handles kept a CI step alive after its process died.
wait_for_new
classmethod
¶
wait_for_new(before: list[WindowSnapshot], timeout: float = 10.0, title_pattern: str | None = None, exclude_hwnds: set[int] | None = None, process_names: tuple[str, ...] | list[str] | None = None, window_classes: tuple[str, ...] | list[str] | None = None, require_all: bool = False, context: str = '', watch_pid: int | None = None, sample_every: float = 5.0, watch_is_target: bool = False) -> Window
Waits for a window that was not in before to appear, and returns it.
With watch_pid, the process is sampled every sample_every seconds
while the wait goes on -- alive, CPU seconds, the windows it owns even
when hidden -- and the samples go to the session journal and into the
timeout message. With watch_is_target the process is the app, so its
exit before any window ends the wait at once with its exit code.
Without it the process may be a launcher (explorer.exe shell:... exits
with code 1 once the packaged app is off; wt.exe hands off to
WindowsTerminal.exe), so its exit is recorded and the wait goes on.
The waiting half of launch_and_discover, for the windows something
other than a launch opens. A Qt menu is the case this was split out for:
the popup is a top-level Qt<ver>QWindowPopup rather than a UIA
descendant of the item that opened it, so MenuItem.sub_items() comes
back empty and the popup has to be found on the desktop instead.
Callers were writing this themselves as snapshot, act, sleep for a
guess, snapshot, take the first added window -- which skips everything
below, and fails as a StopIteration on a line that cannot say what did
not appear when the guess was short.
Matching, require_all and readiness are exactly as in
launch_and_discover, because they are now the same code.
Elements¶
wintegrate.element.UiaElement ¶
Thin, robust wrapper directly over IUIAutomationElement COM interface. No fragile UI control class hierarchies.
bounding_rectangle
property
¶
Returns (left, top, right, bottom) bounding rectangle.
toggle_state
property
¶
0 = off, 1 = on, 2 = indeterminate; None when not a toggleable control.
expand_collapse_state
property
¶
0 = collapsed, 1 = expanded, 2 = partially expanded, 3 = leaf; None if unsupported.
from_handle
classmethod
¶
Resolves an element directly from a native window handle.
get_parent ¶
Navigates to the parent element using UIA ControlViewWalker.
click ¶
Clicks the centre of this element with a synthesised mouse click.
Raises when the element has no bounding rectangle, which is the whole point of this signature. A physical click needs a coordinate; an element that is scrolled out of view, not yet laid out, or hosted in a way that publishes no rectangle has none, and there is nothing sensible to click.
This used to return quietly in that case, and the cost was consistently paid somewhere else: a WinUI flyout button whose rectangle is (0,0,0,0) looked like "focus never reached the rename box"; tree nodes scrolled out of a dialog's viewport looked like "none of the 27 pages had the control"; a list item on a smaller desktop looked like "the selection was stolen". One cause, three unrecognisable symptoms.
Pass require_rectangle=False for the old behaviour where a missing
rectangle is genuinely acceptable — and prefer invoke() where the
element supports it, since an Invoke needs no coordinates at all.
hover ¶
Moves the mouse pointer to the centre of this element.
middle_click ¶
Clicks the centre of this element with a synthesised middle mouse click.
right_click ¶
Clicks the centre of this element with a synthesised right mouse click.
double_click ¶
Double clicks the centre of this element with synthesised left mouse clicks.
mouse_wheel ¶
Sends a vertical mouse wheel event over the centre of this element.
drag_to ¶
drag_to(target: UiaElement, steps: int = 10, delay: float = 0.01, require_rectangle: bool = True) -> bool
Smoothly drags from the centre of this element to the centre of target element.
get_by_role ¶
Finds descendant elements by role from this element.
get_by_text ¶
Finds descendant elements matching text from this element.
get_by_automation_id ¶
Finds descendant elements by automation_id from this element.
get_by_class ¶
Finds descendant elements by class name from this element.
tap ¶
Taps the centre of this element with an injected touch contact.
The touch counterpart of click(), and it needs a rectangle for the same
reason: a contact is a coordinate, and an element that publishes no
rectangle has none.
Returns False when touch cannot be delivered on this host rather than
raising, because that is a property of the machine and not of the call --
check Touch().available() if the distinction matters. Pass an existing
Touch to reuse one digitizer across many taps instead of creating one
per call.
set_focus ¶
Sets focus to this element via UIA SetFocus, then a physical centre click.
The click is not a fallback, despite how this used to read: it happens
whenever click is true, before anything is verified, because it is what
makes focus reliable on controls that ignore UIA SetFocus. There is no cheap
way to know in advance which those are.
It is a real click, though. On a container it lands on whatever is at the
centre, which can select or activate something, and a recording draws a
marker for each one. Pass click=False when the focus change has to have no
side effects, and check the return value: without the click, UIA SetFocus is
the only thing that ran.
find_descendant ¶
find_descendant(automation_id: str | None = None, name_contains: str | None = None, name_exact: str | None = None, class_name: str | None = None, control_type_id: int | None = None, timeout: float = 5.0, required: bool = True) -> UiaElement | None
Finds a descendant matching ALL supplied criteria within a bounded timeout, with a RawViewWalker fallback for UWP/XAML islands.
Criteria are combined with AND: find_descendant(name_contains="Browse",
control_type_id=50000) returns a Button whose name contains "Browse", never
merely the first Button in the tree. name_contains is a case-insensitive
substring match on the element's Name only — UIA property conditions cannot
express substring matching, so it is applied as a filter over the candidates
selected by the other criteria.
With required=False the call returns None instead of raising when nothing
matches — the presence check to use in place of a raise/except pair.
capture ¶
Captures the screen region this element occupies and returns a PIL Image.
Saves to path when given.
Cropped from a desktop capture rather than rendered: an element is not a window, so there is nothing to ask to draw itself.
exists ¶
Returns whether this element is still alive in the UI tree.
Reads a property off the live element: a destroyed or replaced element raises a COM error instead of answering, which is how staleness surfaces.
find_all ¶
find_all(automation_id: str | None = None, name_contains: str | None = None, name_exact: str | None = None, class_name: str | None = None, control_type_id: int | None = None) -> list[UiaElement]
Returns every descendant matching ALL supplied criteria (empty list if none).
set_toggle_verified ¶
Drives a checkbox to the requested state and confirms it landed there.
Toggle() only cycles the state, so a control already in the target state must be left alone; tri-state controls may need more than one step.
select_verified ¶
Selects this item and confirms the selection stuck.
get_selection ¶
Returns the currently selected children of a selection container (list, combo).
expand_verified ¶
Expands or collapses this element and confirms the state changed.
scroll_into_view ¶
Brings this element into its container's viewport via ScrollItemPattern.
Returns False when the element does not support the pattern, which is not an error: a control that never scrolls has nothing to do here.
realize ¶
Materializes a virtualized item so it exists in the UIA tree.
WPF and WinUI collections only create peers for the rows currently on screen; everything else is a placeholder that answers property queries but cannot be invoked or selected. Realize() forces creation. Returns False when the element is not virtualized — the common case, and not a problem.
ensure_available ¶
Makes this element ready to interact with: realize it if virtualized, then scroll it into view. Safe to call on any element; returns self for chaining.
find_item_by_property ¶
find_item_by_property(property_id: int, value, start_after: UiaElement | None = None) -> UiaElement | None
Finds a child through ItemContainerPattern, which searches the container's own data rather than the visual tree.
This is the way to reach an item that virtualization has kept out of the
tree entirely: find_descendant cannot see it, because as far as UIA is
concerned it does not exist yet.
supported_patterns ¶
Names the control patterns this element actually supports.
For diagnosing "the control does not do what I expected": UIA providers differ between control versions and Windows builds, and a list of what is there beats guessing at what is missing.
describe ¶
A one-line identity for error messages: what this element is and can do.
as_data_grid ¶
Views this element as a DataGrid. Raises if it exposes no Grid pattern.
send_physical_keys ¶
Focuses this element and types text as physical (scan-code) key presses,
which an active IME sees and composes.
type_verified and send_char_input inject Unicode codepoints, which reach
the control without passing through the IME — fine for getting characters in,
useless when the IME itself is under test.
click=False focuses without the physical click. Pass it when the caller has
already focused deliberately, or when a click would be visible: a recording
draws a marker for every one, and a run that types ten phrases otherwise
collects ten markers nobody asked for.
send_keys ¶
Focuses this element and sends a SendKeys-style spec ("{ENTER}", "^a", "{TAB 3}").
Unlike type_verified this asserts no post-condition — the caller decides what the keys were supposed to achieve and verifies that.
click=False focuses without the physical click; see send_physical_keys.
read_value ¶
Reads the element's text and reports which source answered.
The sources are tried in order of authority: TextPattern (a document),
ValuePattern (a value-bearing control), WM_GETTEXT (a native window),
and finally Name — which is not the element's text at all but its
label, and is reported as such rather than passed off as content.
Never raises: a reading always comes back, and the source says how much
it is worth. get_value() is the version that refuses the weak answer.
get_value ¶
Reads the element's text, refusing to return its Name in place of it.
When nothing in the element can be asked what text it holds, the only
thing left is the Name — the element's label. Returning that silently is
how a caller ends up asserting against a placeholder: the field driving
this change is a WinUI TextBox whose Name is 'n' (from {n}), so an
empty field read back as 'n' and every "the field is not empty" check
would have passed on nothing.
allow_name_fallback=True opts back into it, for the controls where the
Name genuinely is the displayed text — a ComboBox reflecting its
selection, a grid cell — and read_value() gives the reading plus its
source when the caller would rather decide for itself.
Raises ValueUnavailableError when there is no text source. An empty
string from a real source is returned as '', because that is an answer.
set_value_verified ¶
Sets text via ValuePattern and verifies the value immediately.
invoke ¶
Triggers InvokePattern on the element.
type_verified ¶
type_verified(text: str, expected_line_count_delta: int = 0, verify_contains: str | None = None, delay_per_char: float = 0.03, timeout: float = 8.0, click: bool = True) -> bool
Sends hardware keypresses via send_char_input (SendInput KEYEVENTF_UNICODE), and asserts verified text mutation.
click=False focuses without the physical click; see send_physical_keys.
Note that it applies to every retry below, so a caller that turns it off is
relying on UIA focus working — which is the trade the click exists to avoid.
Input¶
wintegrate.interop.parse_key_spec ¶
Parses a SendKeys-style spec into (kind, value, modifier_vks) actions.
"{ENTER}"/"{ESC}"/"{F5}"-> named virtual keys (kind="vk")"{TAB 3}"repeats a named key three times"^a","+{TAB}","%{F4}"-> Ctrl / Shift / Alt applied to the next key- anything else is literal text (
kind="char"), sent as Unicode "{{"and"}}"are literal braces
Pure function: no Win32 calls, so the grammar is testable on any platform. Raises ValueError on an unterminated brace or an unknown key name.
wintegrate.interop.send_keys ¶
Sends a SendKeys-style spec: named keys in braces, ^/+/% modifiers,
everything else as literal Unicode text. See parse_key_spec for the grammar.
There is no Win-key modifier in this grammar; use send_hotkey("win+...").
Returns False if the system refused to inject any of the events.
wintegrate.interop.send_physical_keys ¶
Types text as physical key presses, so an active IME sees and composes them.
Each character is mapped to a virtual key for the current keyboard layout
(VkKeyScanW) and delivered by scan code, with Shift held where the layout
requires it. Use this — not send_char_input — whenever the IME is the thing
under test; use send_char_input when you only need the characters to land.
Returns False if the system refused to inject any of the events.
wintegrate.interop.send_char_input ¶
Sends character input reliably using native Win32 SendInput (KEYEVENTF_UNICODE).
Note: KEYEVENTF_UNICODE injects the character directly and does NOT pass through an IME. Automating IME composition requires scan-code input instead.
wintegrate.interop.send_vk_input ¶
Presses a virtual key, holding modifiers (Ctrl/Shift/Alt VKs) around it.
Touch¶
wintegrate.touch.Touch ¶
Injects touch contacts, and gestures built out of them.
A single instance owns one synthetic digitizer, created on first use and
destroyed with close(). Reuse it: creating a device per tap works, but the
device shows up in GetPointerDevices and churning them is noise.
available ¶
Whether an injected contact actually reaches a window.
Not "is the API there": that question has a different answer. This taps a hidden window of our own and reports whether the tap arrived, because a host can accept every injection and deliver none -- which is what a desktop covered by another window looks like from here.
Cached after the first call; pass force=True to measure again, which
is worth doing if the desktop has been cleared since.
tap ¶
Presses and lifts one contact. Returns whether every frame was accepted.
double_tap ¶
Two taps at one point, interval apart.
The interval has to stay under the system's double-click time, which is
a user setting (GetDoubleClickTime, 500 ms by default) -- so this reads
it rather than assuming, and clamps.
long_press ¶
Holds one contact still, re-sending frames for the whole duration.
The frames are not decoration: a contact that is injected once and then left alone is a finger that stopped reporting, and press-and-hold recognition wants to see it stay.
swipe ¶
swipe(start_x: int, start_y: int, end_x: int, end_y: int, steps: int = 12, frame_interval: float = DEFAULT_FRAME_INTERVAL) -> bool
Drags one contact along a straight line.
Whether this pans, scrolls, flicks or selects is up to what is underneath and how far it travelled -- pan thresholds are system metrics. This guarantees the trajectory, not the interpretation.
contacts ¶
Puts several contacts down and yields them, for hand-written gestures.
Use this when the shape is not one of the gestures below:
with touch.contacts([(100, 100), (300, 300)]) as (a, b):
a.move_to(120, 120)
b.move_to(280, 280)
Frames are whole-hand, so Contact.move_to sends the positions of every
open contact -- one finger cannot move without the others being restated,
or they would read as lifted.
pinch ¶
pinch(centre_x: int, centre_y: int, start_radius: int, end_radius: int, steps: int = 12, angle_degrees: float = 0.0, frame_interval: float = DEFAULT_FRAME_INTERVAL) -> bool
Moves two contacts along a line through a centre, together or apart.
end_radius > start_radius spreads; smaller pinches. Nothing here
promises a zoom -- see this module's docstring.
rotate ¶
rotate(centre_x: int, centre_y: int, radius: int, degrees: float, steps: int = 16, frame_interval: float = DEFAULT_FRAME_INTERVAL) -> bool
Turns two opposed contacts about a centre by degrees.
wintegrate.touch.Contact ¶
One finger, while a Touch.contacts() block is open.
Holds only its own id and position. Frames are whole-hand, so moving a
contact means asking the owning Touch to send a frame describing all of
them -- a contact cannot move on its own without lifting the others.
move_to ¶
Moves this contact and sends one frame for the whole hand.
IME and keyboard layout¶
wintegrate.interop.get_ime_status ¶
Reads the IME state attached to a window: open/closed and conversion mode.
has_context is False when the window has no IMM32 context at all — which is
the normal answer for a modern XAML/WinUI control, where text services run
through TSF instead. Treat a False there as "ask TSF", not as "IME is off".
has_context is False whenever the control routes text services through TSF
rather than IMM32 — which modern controls do — so it cannot distinguish "no
IME here" from "an IME is running and swallowing your keystrokes". There is
deliberately no companion field claiming to answer that: ImmIsIME looked
like the answer but reports whether an HKL is loaded, returning true for a
plain en-GB layout the moment it is loaded alongside an IME. Switch the
layout to get deterministic input; do not try to detect an IME. A Bopomofo layout swallows
unshifted letters into composition whether or not IMM32 hands out a context,
so "no context" and "no IME" are different facts and a caller needs both.
wintegrate.interop.set_ime_open ¶
Opens or closes the IME for a window.
Goes through WM_IME_CONTROL rather than ImmSetOpenStatus. ImmGetContext returns nothing for a window in another process — and for anything routing text services through TSF — so the context-based call silently does nothing in exactly the case an automation tool cares about. The default IME window accepts the request across process boundaries.
wintegrate.interop.set_ime_conversion ¶
Sets the IME conversion mode (see the IME_CMODE_* flags).
sentence is accepted for symmetry with get_ime_status and is not sent:
WM_IME_CONTROL carries the conversion mode only.
Measured on a zh-TW Windows 11 ARM64 desktop, driving a dialog in another process: with IME_CMODE_ALPHANUMERIC, send_physical_keys("hello") lands "hello"; with IME_CMODE_NATIVE the same call lands "" because the IME takes the keystrokes into composition. That is the switch this function exists to give a test.
wintegrate.interop.get_composition_string ¶
Returns the IME's in-progress composition string for a window ("" when idle).
This is the direct evidence that keystrokes reached the IME rather than the control: mid-composition the characters live here, not in the control's value.
wintegrate.interop.get_keyboard_layout ¶
Returns the active keyboard layout (HKL) for a window's thread, or this thread's.
Per-window IME state is a property of the thread owning the window, which is why this takes an hwnd rather than assuming the caller's thread.
wintegrate.interop.get_keyboard_layout_list ¶
Returns every keyboard layout (HKL) currently loaded in the session.
Diagnostics¶
wintegrate.diagnostics.ContinuousRecorder ¶
Low-memory streaming desktop screen recorder.
Encodes in-process through PyAV, which bundles FFmpeg and is the only route
that works everywhere this library runs: no other ffmpeg distribution on PyPI
ships a win_arm64 wheel, so shelling out to an external binary would mean
asking Windows ARM64 users to install one themselves.
Recording is a diagnostic, never the thing under test. When PyAV is missing,
start() reports failure and the caller carries on without a video rather
than a run failing over a missing artifact.
anchor ¶
How to map a timestamp in the event timeline onto the recording.
Frames are stamped pts_ms = (monotonic - monotonic_start) * 1000, and
events carry both monotonic and a wall clock, so
video_ms = (event.monotonic - monotonic_start) * 1000. Also says what the
frame covers: the primary monitor at its native size, not the virtual
desktop that the screenshots cover -- a mismatch that is otherwise written
nowhere.
wintegrate.diagnostics.WindowCensus ¶
Takes instant snapshots of all top-level desktop windows and diffs them.
wintegrate.recorder.TextActionTimelineRecorder ¶
Records a high-fidelity, human-readable structured text timeline (.log / .jsonl / .txt) of all UI actions, state mutations, and window focus transitions.
Extremely lightweight for CI runs where video capture might be disabled or where instant diffing/grepping in PR comments / CI terminal output is desired.
wintegrate.recorder.ActionPlayer ¶
Plays back a sequence of recorded structured actions or a recorded session.
playback ¶
Replays all actions sequentially.
Environment¶
wintegrate.env.PlatformCapabilities
dataclass
¶
Represents detected OS edition, architecture, and subsystem support.
wintegrate.env.is_ci ¶
Returns True when running under a CI service (CI / GITHUB_ACTIONS truthy).
Exceptions¶
wintegrate.exceptions ¶
Exceptions hierarchy for wintegrate.
WintegrateError ¶
Bases: Exception
Base exception for all wintegrate errors.
Keyword arguments beyond diagnostics are facts about the failure, kept on
the instance and used to build signature. Only the keys a class lists in
SIGNATURE_KEYS take part, so two runs that failed the same way produce the
same signature: no hwnd, pid or timestamp may appear in that list -- one
per-run integer makes every signature unique and destroys the comparison it
exists for. The list is reviewed with the class.
signature
property
¶
ClassName[key=value,...], from the class's allow-listed facts only.
FocusStealDetectedError[foreground_image=explorer.exe] says what
FocusStealDetectedError alone does not: two of them with different
foregrounds are two problems, not one that happened twice.
WindowDiscoveryTimeoutError ¶
Bases: WintegrateError
Raised when a target window cannot be discovered within the configured timeout.
ElementNotFoundError ¶
ActionVerificationError ¶
ActionTimeoutError ¶
TextMismatchError ¶
FocusStealDetectedError ¶
Bases: ActionVerificationError
Raised when focus was stolen away during an active verification or typing sequence.
ArtifactMissingError ¶
Bases: WintegrateError
Raised when a file another process should have written did not arrive in time.
Carries the directory listing at the deadline, so the negative says what it examined.
DiagnosticPipelineError ¶
ValueUnavailableError ¶
Bases: WintegrateError
Raised when an element holds no readable text source.
Not "the text is empty" — that is an answer, and it is returned as ''. This
is "nothing in this element can be asked what text it holds", where the only
thing left to report would be the element's Name, which is its label.
Substituting a label for content is how an empty field comes back looking
like it has something in it.
ConsoleHostEndedError ¶
Bases: WintegrateError, KeyboardInterrupt
The console this process was attached to was destroyed while the session was starting.
What reaches the traceback is a KeyboardInterrupt at whatever line the
process happened to be on: destroying a console delivers CTRL_C_EVENT to every
process attached to it. This class says what that interrupt was. It is still a
KeyboardInterrupt, so a pytest run aborts once, as it should on a machine
whose console is gone, instead of producing one error per remaining test.
Raised only on evidence: the console probes that answered at preflight no longer do. A real Ctrl-C with the console intact is re-raised untouched.