Migration audit/ X11::GUITest 0.29/ 50 exported functions

X11::GUITest on Wayland

Every symbol in @EXPORT_OK, classified by what it would actually take to make it work in a Wayland session. Derived from the module source in GUITest.pm and GUITest.xs — not from the documented feature list, which turns out to describe a different module than the one in the tree. Thirteen of fifty carry over unchanged.

Where the surface lands

The distribution is the finding

The largest block is not "impossible" — it is "possible, but you must write it once per desktop." Window management is where a portable Wayland implementation actually fails.

T1 PortableT2 DirectT3 Compositor T4 PrivilegedT5 ReworkT6 No path
13
Port unchanged — process control, delays, string quoting, screen geometry.
19
Need a separate backend for GNOME, KDE and wlroots. All window-related.
8
Input injection. One mechanism, gated on consent or device permissions.
6
No path for an unprivileged client on any compositor. Drop from the API.
Classification

Six tiers, ordered by cost

The tiers are a scale, not a set of labels: each one costs strictly more to implement than the one above it, and T6 cannot be bought at any price without privileged access.

T1 PORTABLE
No display server involved Pure Perl or libc. Runs identically on Linux, the BSDs, Solaris and Windows. Free.
T2 DIRECT
Core Wayland protocol A stable, universally implemented interface gives a real equivalent. Write once.
T3 COMPOSITOR
Per-desktop backend No universal protocol. Needs distinct code for wlroots, KWin and Mutter — and Mutter implements none of the foreign-toplevel protocols at all.
T4 PRIVILEGED
Consent or device access Portal dialog, membership of the input group, or a wlroots-only protocol. Never silent on GNOME or KDE.
T5 REWORK
Goal survives, model does not The X11 mechanism has no analogue, but the reason you called it is served better by AT-SPI. Same intent, different API shape.
T6 NO PATH
Deliberately prevented Wayland exposes no way to do this, by design, on any compositor. Not a gap awaiting a protocol.
T1 · 9 functions

Process control and module state

These never touched the X server. They are the free half of the port, and the only part that keeps X11::GUITest's existing FreeBSD and Solaris support intact.

FunctionX11 implementationWayland pathTier
StartAppfork / execsubprocess.Popen — unchangedT1
RunAppsystem()subprocess.run — unchangedT1
WaitSecondssleep()time.sleep — unchangedT1
QuoteStringForSendKeysstring substitutionPure string handling. Keep only if the {} SendKeys grammar is keptT1
QSfSKaliasAlias of the above. Drop the abbreviation in a new APIT1
SetEventSendDelaymodule globalModule state. Better expressed as a constructor argumentT1
GetEventSendDelaymodule globalModule stateT1
SetKeySendDelaymodule globalModule stateT1
GetKeySendDelaymodule globalModule stateT1
T2 · 4 functions

Screen properties

The one area where Wayland's core protocol answers the same question X11 did. Every compositor implements wl_output; no permission, no backend.

FunctionX11 implementationWayland pathTier
GetScreenResDisplayWidth / Heightwl_output.mode — direct equivalent, and reports scale, transform and refresh besidesT2
ScreenCountScreenCount()wl_output globals in the registry. Note the semantics shift: X11 screens are rare, Wayland outputs are just monitorsT2
DefaultScreenDefaultScreen()No such concept. Return the first advertised output by conventionT2
GetScreenDepthDefaultDepth()Not exposed — and no longer variable. Every compositor composites 8 bits per channel; return 24 or 32 as a constant, or drop itT2
T4 · 8 functions

Input injection

Mechanically the easiest group to make work and the most consequential to get wrong. All eight reduce to one backend decision, discussed under the keymap trap below.

FunctionX11 implementationWayland pathTier
MoveMouseAbsXTestFakeMotionEventNotifyPointerMotionAbsolute via the RemoteDesktop portal, libei, or a uinput device with ABS_X/ABS_Y. Relative-only injection cannot implement thisT4
PressMouseButtonXTestFakeButtonEventNotifyPointerButton / EV_KEY BTN_LEFTT4
ReleaseMouseButtonXTestFakeButtonEventAs above, release stateT4
ClickMouseButtonpress + release pairComposed from the two above. Buttons 4 and 5 must become axis events, not button events — scroll is not a button on WaylandT4
PressKeyXTestFakeKeyEventNotifyKeyboardKeycode / EV_KEY. Takes a keycode, so the keymap problem appliesT4
ReleaseKeyXTestFakeKeyEventAs above, release stateT4
PressReleaseKeypress + release pairComposed from the two aboveT4
SendKeysXKeysymToKeycode + XGetKeyboardMappingThe {} grammar and modifier synthesis port cleanly; the keysym-to-keycode lookup does not. There is no Wayland call that answers "which keycode produces this character"T4
T3 · 19 functions · the bulk of the work

Windows: discovery and control

Two protocols cover part of this — ext-foreign-toplevel-list-v1 (read-only: title, app id, identifier) and wlroots' zwlr_foreign_toplevel_management_v1 (adds activate, minimize, close). KWin and wlroots implement them. Mutter implements neither, so on GNOME — the likely default session for most users — every row below needs a Shell extension or a D-Bus Eval call that is itself locked down. Neither protocol reports window geometry at any point.

FunctionX11 implementationWayland pathTier
GetWindowNameXFetchName, _NET_WM_NAMEtoplevel.title event. Arrives asynchronously and is pushed, not polled — a genuine improvementT3
FindWindowLikeXQueryTree + regex on WM_NAMERegex over the toplevel list. Returns only toplevels — the recursive descent into child windows this relies on has no equivalent (see T5)T3
WaitWindowLikepoll FindWindowLikeReplace polling with the toplevel creation event. Faster and race-freeT3
WaitWindowClosepoll IsWindowtoplevel.closed event. Strictly better than the polling loop it replacesT3
WaitWindowViewablepoll XGetWindowAttributesApproximate via the minimized state flag. "Viewable" as X11 defines it — mapped, with all ancestors mapped — has no counterpartT3
IsWindowXGetWindowAttributesLiveness of a toplevel handle. Handles are objects, not reusable integer IDs — safer than X11 window IDsT3
IsWindowViewableIsViewable attributeminimized state flag, with the same caveat as WaitWindowViewableT3
GetWindowPid_NET_WM_PID propertyNot carried by either foreign-toplevel protocol. KWin scripting or a GNOME extension only. Frequently unavailableT3
GetWindowsFromPidscan tree for _NET_WM_PIDInherits the above. Consider matching on app_id instead — it is always present and more stable than a pidT3
GetWindowPosXTranslateCoordinates + XGetWindowAttributesNo protocol reports this. A client is not told where it is on screen, and no protocol tells a third party either. Compositor scripting onlyT3
MoveWindowXMoveWindowNo protocol. KWin scripting or a GNOME extension. Placement is the compositor's prerogativeT3
ResizeWindowXResizeWindowNo protocol. Same compositor-scripting escape hatchT3
RaiseWindowXRaiseWindowApproximate with activate, which also transfers focus. There is no raise-without-focus operationT3
SetInputFocusXSetInputFocustoplevel.activate, requires a seat. The compositor may refuse or defer it under focus-stealing policyT3
GetInputFocusXGetInputFocusThe activated state flag on wlroots toplevels. Absent from ext-foreign-toplevel-list-v1, which carries no state at allT3
IconifyWindowXIconifyWindowset_minimized. One of the few window operations with a real protocol callT3
UnIconifyWindowXMapWindowunset_minimizedT3
GetWindowFromPointstacking-order scan of child geometryRequires per-window geometry and stacking order, neither of which is exposed. Compositor scripting onlyT3
ClickWindowGetWindowPos + MoveMouseAbs + clickInherits the geometry gap and the injection permission both. Replace with an AT-SPI element click, which needs neitherT3
T5 · 4 functions

The window tree

X11 exposes every widget as a nestable window, and X11::GUITest is built on walking that tree. Wayland has no tree: one surface per toplevel, and what is inside it is the toolkit's private business. This is the deepest architectural break in the module — and the one already anticipated in the repository's own ToDo file, which names AT-SPI as the fix.

FunctionX11 implementationReplacementTier
GetRootWindowRootWindow()No root window exists. The nearest concept is the desktop, an AT-SPI application collectionT5
GetChildWindowsrecursive XQueryTreeRecurse the AT-SPI accessible tree instead: real roles and labels rather than anonymous window IDsT5
GetParentWindowXQueryTree parentAT-SPI parent accessorT5
IsChildscan GetChildWindowsAT-SPI ancestry checkT5

This break is not new with Wayland. Client-side decorations and toolkit-drawn widgets already made the X11 window tree mostly empty for modern GTK and Qt applications — a single window where X11::GUITest expects dozens. AT-SPI is the correct answer under X11 too.

T6 · 6 functions

No path at any privilege short of the compositor

These are not awaiting a protocol. Each one is a capability Wayland removed on purpose, because it lets one client observe or impersonate another. Do not carry them into the new API with a stub that always fails — leave them out and document why.

FunctionX11 implementationWhy it cannot workTier
GetMousePosXQueryPointerNo protocol reports the global pointer position; a client learns coordinates only inside its own surface, only while pointed at. Injection is possible, readback is not — track the position you last setT6
IsKeyPressedXQueryKeymapGlobal keyboard state is exactly what a keylogger reads. Only readable by opening /dev/input/event* directly, which is an OS bypass, not a Wayland pathT6
IsMouseButtonPressedXQueryPointer maskAs above — global input state is not observableT6
SetWindowNameXSetWMName, XSetWMIconNameA title belongs to the client that owns the surface. Nothing can rewrite another application's title — that is impersonation, and the protocol has no request for itT6
LowerWindowXLowerWindowNo foreign-toplevel protocol offers a lower or restack operation. Only activate exists, and only upwardT6
IsWindowCursorXTestCompareCursorWithWindowCursor shape is negotiated privately between client and compositor. No protocol, no portal, no scripting interface reports it. No Wayland workaround exists — though X11 keeps XTestCompareCursorWithWindow, so this one survives on that backend aloneT6
Corrections

What the design note got wrong about the module

The capability list in the planning document does not match @EXPORT_OK. Four claims are wrong in a direction that matters for scoping.

  1. There is no screenshot function. Nothing in the 50 exports captures pixels; there is no image handling in the module at all. Mapping "screenshot" to the desktop portal describes a new feature, not a port — which is good news, because the portal makes it easy, but it should not be counted as compatibility work.
  2. Windows are not matched by class. FindWindowLike matches WM_NAME and _NET_WM_NAME only; WM_CLASS appears nowhere in the source. Wayland's app_id is in fact a better match key than anything the module has today — worth adding rather than reproducing.
  3. There is no application enumeration. Discovery is a recursive XQueryTree walk plus _NET_WM_PID lookups. The distinction matters: what needs replacing is a tree walk, not an application list.
  4. About half the surface is unlisted. Missing from the note: the four delay accessors, the three input-state queries, IsWindowCursor, GetScreenDepth, ScreenCount, StartApp, RunApp and the SendKeys quoting helpers — 15 of 50.
Gaps in the plan

Three mechanisms the note does not mention

The note proposes uinput as the input backend. That was the right answer several years ago and is now the fallback, not the primary.

libei

The emulated-input stack Wayland compositors actually adopted — supported by Mutter and KWin, brokered through the portal so it needs no root and no device nodes. This should be the primary input backend, with uinput kept only for compositors and BSDs that lack it.

Reaches native Wayland clients, which XTest under XWayland never will.

The RemoteDesktop portal

org.freedesktop.portal.RemoteDesktop grants pointer and keyboard injection after one consent dialog, and pairs with ScreenCast to get capture in the same session. Cross-desktop, unprivileged, and the only sanctioned route on GNOME.

Consent is per session — design the API so a test suite acquires it once at startup, not per call.

Virtual device protocols

zwp_virtual_keyboard_manager_v1 and zwlr_virtual_pointer_manager_v1 give unprivileged injection on wlroots compositors with no dialog at all — the closest thing to XTest's old ergonomics that still exists.

Also the cleanest fix for the keymap problem, because the client supplies its own keymap.

The keymap trap

uinput injects scancodes below the compositor, so the compositor applies whatever xkb layout is active. SendKeys("Hello") on an AZERTY or Dvorak session types different characters — and no protocol lets you ask which layout is in effect.

X11::GUITest avoided this with XKeysymToKeycode against the server's live map. libei and virtual-keyboard both restore that guarantee; raw uinput cannot.

Consequence

What this implies for the new package

  • Keep the X11 backend as a peer, not a legacy path. It is the only backend that can serve FreeBSD and Solaris — libei and uinput are Linux interfaces, and neither of those systems runs a Wayland compositor you would target. The existing XS code keeps its value.
  • Make capability negotiation part of the public API. With 19 functions varying by compositor and 6 unavailable everywhere, a call that silently returns zero — the current failure convention — becomes untestable. Let callers ask what the active backend supports before they depend on it.
  • Lead with AT-SPI, not coordinates. It is the only layer that answers the question the window-tree functions were really being used for, it is unaffected by the T3 and T6 gaps entirely, and it works identically under X11 and Wayland — which makes it the one part of the design that does not need a backend matrix.
  • Do not reproduce the 1-to-1 API. Of 50 functions, 13 port unchanged, 4 want new semantics, 6 should not exist, and the rest change shape. A faithful port would spend most of its effort on the parts worth redesigning.