Skip to content

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.

mouse property

mouse

Playwright-style Mouse controller associated with this session.

log_event

log_event(event_type: str, message: str, **kwargs)

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

step(name: str)

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

capture_screenshot(name: str, window: Window | None = None, all_monitors: bool = True) -> Path

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.

mouse property

mouse

Playwright-style Mouse controller for this application.

locator

locator(selector: str | dict)

Returns a Playwright-style Locator rooted at this application's main window.

get_by_role

get_by_role(role: str, name: str | None = None, exact: bool = False)

Finds elements by role in this application's main window.

get_by_text

get_by_text(text: str, exact: bool = False)

Finds elements by text in this application's main window.

get_by_automation_id

get_by_automation_id(auto_id: str)

Finds elements by automation_id in this application's main window.

get_by_class

get_by_class(class_name: str)

Finds elements by class name in this application's main window.

close

close() -> None

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

is_visible: bool

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

cloak_reason: CloakReason | None

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

is_cloaked: bool | None

Whether DWM is hiding this window. None when it cannot be determined.

display_affinity property

display_affinity: DisplayAffinity | None

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

is_excluded_from_capture: bool | None

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

is_on_screen: bool

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

keyboard_layout: int

The active keyboard layout (HKL) of the thread that owns this window.

keyboard_language_id property

keyboard_language_id: int

The LANGID (low word of the HKL) of this window's keyboard layout.

ime_mode

ime_mode(conversion: int)

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

foreground(verify: bool = True, timeout: float = 2.0)

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

get_ime_status() -> dict[str, object]

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

set_ime_open(is_open: bool) -> bool

Opens or closes this window's IME. False when it has no IMM32 context.

set_ime_conversion

set_ime_conversion(conversion: int, sentence: int = 0) -> bool

Sets this window's IME conversion mode (see the IME_CMODE_* flags).

get_composition_string

get_composition_string() -> str

Returns the IME's in-progress composition text for this window ("" when idle).

set_keyboard_layout_verified

set_keyboard_layout_verified(layout_id: str, timeout: float = 3.0) -> int

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

capture(path: str | Path | None = None)

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

exists(require_visible: bool = True, require_on_screen: bool = False) -> bool

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

re_resolve_element() -> UiaElement

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

locator

locator(selector: str | dict)

Returns a Playwright-style Locator rooted at this window.

get_by_role

get_by_role(role: str, name: str | None = None, exact: bool = False)

Finds descendant elements in this window by role (e.g. 'button', 'checkbox', 'tab', 'menuitem').

get_by_text

get_by_text(text: str, exact: bool = False)

Finds descendant elements in this window matching text.

get_by_automation_id

get_by_automation_id(auto_id: str)

Finds descendant elements in this window by UIA automation_id.

get_by_class

get_by_class(class_name: str)

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

find_checkbox(automation_id: str | None = None, name: str | None = None, timeout: float = 5.0)

Locates a CheckBox in this window.

find_tab_control

find_tab_control(automation_id: str | None = None, name: str | None = None, timeout: float = 5.0)

Locates a TabControl in this window.

find_menu

find_menu(automation_id: str | None = None, name: str | None = None, timeout: float = 5.0)

Locates a Menu / MenuBar in this window.

find_combobox

find_combobox(automation_id: str | None = None, name: str | None = None, timeout: float = 5.0)

Locates a ComboBox in this window.

focus_content_island

focus_content_island(timeout: float = 3.0) -> bool

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 InputNonClientPointerSource input 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

ensure_onscreen(margin: int = 0) -> bool

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

move_to_current_desktop()

Moves this window to the currently active virtual desktop if pyvda is available.

set_foreground

set_foreground(verify: bool = True, timeout: float = 2.0) -> bool

Brings the window to the foreground cleanly using Win32 AttachThreadInput. Synchronizes thread input queues to reliably grant foreground activation permission.

maximize

maximize(verify: bool = True, timeout: float = 5.0) -> bool

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.

restore

restore() -> None

Undoes maximize, through the same message path.

move_and_resize

move_and_resize(x: int, y: int, width: int, height: int, repaint: bool = True)

Reposition window on screen.

close

close(force: bool = False, timeout: float = 3.0)

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

bounding_rectangle: tuple[int, int, int, int]

Returns (left, top, right, bottom) bounding rectangle.

toggle_state property

toggle_state: int | None

0 = off, 1 = on, 2 = indeterminate; None when not a toggleable control.

expand_collapse_state property

expand_collapse_state: int | None

0 = collapsed, 1 = expanded, 2 = partially expanded, 3 = leaf; None if unsupported.

is_enabled

is_enabled() -> bool

Returns True if this element is enabled.

is_visible

is_visible() -> bool

Returns True if this element is visible and not offscreen.

from_handle classmethod

from_handle(hwnd: int) -> UiaElement

Resolves an element directly from a native window handle.

get_root classmethod

get_root() -> UiaElement

Resolves the Desktop RootElement directly.

get_focused classmethod

get_focused() -> UiaElement

Resolves the currently focused UIA element.

get_parent

get_parent() -> UiaElement | None

Navigates to the parent element using UIA ControlViewWalker.

click

click(require_rectangle: bool = True)

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

hover(require_rectangle: bool = True, steps: int = 1, delay: float = 0.0) -> bool

Moves the mouse pointer to the centre of this element.

middle_click

middle_click(require_rectangle: bool = True) -> bool

Clicks the centre of this element with a synthesised middle mouse click.

right_click

right_click(require_rectangle: bool = True) -> bool

Clicks the centre of this element with a synthesised right mouse click.

double_click

double_click(require_rectangle: bool = True, interval: float = 0.05) -> bool

Double clicks the centre of this element with synthesised left mouse clicks.

mouse_wheel

mouse_wheel(delta: int, require_rectangle: bool = False) -> bool

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.

locator

locator(selector: str | dict)

Returns a Playwright-style Locator rooted at this element.

get_by_role

get_by_role(role: str, name: str | None = None, exact: bool = False)

Finds descendant elements by role from this element.

get_by_text

get_by_text(text: str, exact: bool = False)

Finds descendant elements matching text from this element.

get_by_automation_id

get_by_automation_id(auto_id: str)

Finds descendant elements by automation_id from this element.

get_by_class

get_by_class(class_name: str)

Finds descendant elements by class name from this element.

tap

tap(require_rectangle: bool = True, touch=None) -> bool

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

set_focus(verify: bool = True, timeout: float = 2.0, click: bool = True) -> bool

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

capture(path=None)

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

exists() -> bool

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.

children

children() -> list[UiaElement]

Returns this element's direct children (control view).

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

toggle

toggle() -> bool

Advances the toggle state by one step. Returns False if unsupported.

set_toggle_verified

set_toggle_verified(checked: bool, timeout: float = 2.0) -> bool

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

select_verified(timeout: float = 2.0) -> bool

Selects this item and confirms the selection stuck.

get_selection

get_selection() -> list[UiaElement]

Returns the currently selected children of a selection container (list, combo).

expand_verified

expand_verified(expand: bool = True, timeout: float = 2.0) -> bool

Expands or collapses this element and confirms the state changed.

scroll_into_view

scroll_into_view() -> bool

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

realize() -> bool

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

ensure_available() -> UiaElement

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

supported_patterns() -> list[str]

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

describe() -> str

A one-line identity for error messages: what this element is and can do.

as_data_grid

as_data_grid()

Views this element as a DataGrid. Raises if it exposes no Grid pattern.

as_tree_view

as_tree_view()

Views this element as a TreeView.

as_tree_item

as_tree_item()

Views this element as a single TreeView item.

send_physical_keys

send_physical_keys(text: str, delay_per_key: float = 0.03, click: bool = True) -> bool

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

send_keys(spec: str, delay_per_key: float = 0.02, click: bool = True) -> bool

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

read_value() -> ValueReading

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

get_value(allow_name_fallback: bool = False) -> str

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

set_value_verified(text: str, timeout: float = 3.0) -> bool

Sets text via ValuePattern and verifies the value immediately.

invoke

invoke(verify_closed: bool = False, timeout: float = 3.0) -> bool

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

parse_key_spec(spec: str) -> list[tuple[str, object, tuple[int, ...]]]

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

send_keys(spec: str, delay_per_key: float = 0.02) -> bool

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

send_physical_keys(text: str, delay_per_key: float = 0.03) -> bool

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

send_char_input(char: str)

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

send_vk_input(vk: int, modifiers: tuple[int, ...] = ()) -> bool

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

available(force: bool = False) -> bool

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

tap(x: int, y: int, duration: float = DEFAULT_TAP_DURATION) -> bool

Presses and lifts one contact. Returns whether every frame was accepted.

double_tap

double_tap(x: int, y: int, interval: float = 0.08) -> bool

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

long_press(x: int, y: int, duration: float = DEFAULT_LONG_PRESS_DURATION) -> bool

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

contacts(points: list[tuple[int, int]])

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

move_to(x: int, y: int) -> bool

Moves this contact and sends one frame for the whole hand.

IME and keyboard layout

wintegrate.interop.get_ime_status

get_ime_status(hwnd: int) -> dict[str, object]

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

set_ime_open(hwnd: int, is_open: bool) -> bool

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

set_ime_conversion(hwnd: int, conversion: int, sentence: int = 0) -> bool

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

get_composition_string(hwnd: int, index: int = GCS_COMPSTR) -> str

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

get_keyboard_layout(hwnd: int | None = None) -> int

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

get_keyboard_layout_list() -> list[int]

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.

backend property

backend: str | None

"pyav", or None when recording is not running.

anchor

anchor() -> dict | None

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

playback(session: Any = None, speed_factor: float = 1.0)

Replays all actions sequentially.

Environment

wintegrate.env.PlatformCapabilities dataclass

Represents detected OS edition, architecture, and subsystem support.

wintegrate.env.is_ci

is_ci() -> bool

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
signature: str

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

Bases: WintegrateError

Raised when a UIA element or descendant cannot be located.

ActionVerificationError

Bases: WintegrateError

Raised when an action is executed but its post-condition cannot be verified.

ActionTimeoutError

Bases: ActionVerificationError

Raised when waiting for an element state or condition times out.

TextMismatchError

Bases: ActionVerificationError

Raised when text typing or value setting results in a mismatch.

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

Bases: WintegrateError

Raised when a diagnostic subsystem (e.g. streaming recorder) fails.

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.