Skip to main content
QA Wolf runs Windows flows on a Windows 11 desktop VM. A flow drives native apps through the Appium Windows driver and Chrome through Playwright, and one flow can use both. Each run gets a fresh VM, and QA Wolf discards it when the run ends. Nothing a run installs or changes carries over to the next one.
Windows desktop testing is turned on per workspace. Ask your QA Wolf representative to enable it. Until then, a Windows flow fails to start with Windows desktop testing is not enabled for your team.

What the VM comes with

  • The desktop — Windows 11 at 1920×1080, signed in as the user qawolf.
  • Chrome — what launch() connects to. Its version is pinned and updates are off.
  • Adobe Acrobat Reader — pinned the same way.
  • The Appium Windows driver — what startApp() drives native apps through.
A flow installs your own app during the run, as Start your app describes. A private pool can have it installed already.

Choose a target

Ask your QA Wolf representative for a private pool. A flow on the private target fails to start until the pool exists.

Start your app

startApp() opens an app and returns a WebdriverIO driver for it. What you pass decides how the app gets onto the VM: QA Wolf installs an .msi before the session opens, and the session starts the program it installed. Any other file launches as it is. Example:

When an installer leaves several programs

If the .msi installs more than one program, name the one to start with qawolf:launch. Without it, the start fails and the error lists the programs the installer left.
An install that runs longer than 15 minutes fails, and the run log shows the end of the installer’s log.

Use the browser and apps together

launch: true in the flow configuration connects to Chrome on the VM and passes page, context and browser to the callback. On Android and iOS, launch: true starts the app instead. On Windows, call startApp() for each native app. Each call returns its own driver, and apps started earlier stay open. Example:
expect from @qawolf/flows/windows takes either kind of subject. A WebdriverIO driver or element gets the WebdriverIO matchers, and a Playwright page or locator gets the Playwright ones.

Find elements

Prefer an element’s AutomationId. app.$("~SubmitButton") finds the element whose AutomationId is SubmitButton. Skip numeric AutomationIds. Win32 and Delphi apps put window handles there, and those change every launch. Match on Name with XPath over the UI Automation tree instead:
In the flow editor, the Elements tab shows the UI Automation tree of a window you choose from its toolbar. It works while no flow is running.

Legacy Win32 apps

  • Delphi and other VCL apps often leave edit fields without a Name, and list them in creation order rather than screen order. Find the fields by class inside a named pane, and sort them by getLocation().
  • Some custom edit fields reject setValue(). Click the field, then type with app.execute("windows: keys", { actions: [{ text: "..." }] }).
  • Window titles often carry app state. Wait on getTitle() before driving a window.
Example:

Move files on and off the VM

Team storage and uploaded builds are mounted on the runner, not on the VM. To open a file in a desktop app, copy it onto the VM with the driver’s pushFile(), which takes the contents as base64. Read a file back with pullFile(), which returns base64. Example:

Keep screenshots stable

An app’s window can open at a different size from one run to the next, which breaks screenshot comparisons and coordinates. Pin it with qawolf:windowSize:
The start fails if the app won’t take that size. A baseline doesn’t record the window size, so an unpinned window that opens at another size fails the comparison rather than saving a new baseline. See Native mobile screenshots for the toHaveScreenshot options, which are the same on Windows.

Test every new build from CI

A Windows build takes the same path as Mobile build testing. Your pipeline uploads the build, reports a deployment that carries the build’s path as RUN_INPUT_PATH, and a trigger starts the runs. Follow that page with your .msi in place of the .apk, through the GitHub Actions, Node.js or Webhook steps. If Android or iOS builds report into the same environment, give the Windows build its own service and its own trigger. The difference is in the flow. startApp() doesn’t fall back to RUN_INPUT_PATH the way the mobile launch() does, so pass it in. Falling back to the build’s file name lets a run started by hand use the latest upload:
startApp() takes either the absolute path under /home/wolf/run-inputs-executables/ that Mobile build testing uses, or the bare file name.

Drive a Windows desktop from a coding agent

A coding agent connected through the QA Wolf MCP launches a Windows runner with runner_launch and runnerName: "windows". The runner has no screen until its first flow run claims a VM, and until then the screen tools answer screen-needs-a-run. After that, runner_takeScreenshot, runner_performAction and runner_inspectMobile work on the whole desktop. tap and fill also take a UI Automation XPath selector. navigate is refused, so open pages from a flow.

Troubleshooting

  • Windows desktop testing is not enabled for your team. — Windows isn’t turned on for the workspace. Ask your QA Wolf representative to enable it.
  • Device claim is unfulfillable: no devices exist matching the requested selector. — no pool serves the flow’s target. Usually the flow is on Windows 11 - Desktop (private) and the workspace has no private pool. The run fails about 30 seconds after it starts.
  • Timeout while waiting for device allocation — every VM in the pool stayed busy for 30 minutes. While it waits, the run log shows Waiting for device allocation. Queue position: N.
  • The run lost its VM while paused at a breakpoint — QA Wolf reclaims a VM after 60 seconds with no activity from the run.
  • installed several programs, so name the one to start with "qawolf:launch" — the .msi left more than one program. See When an installer leaves several programs.
  • "qawolf:windowSize" needs an app to size — a session over the whole desktop has no window to size. Pass appium:app or appium:appTopLevelWindow with it.
  • A selector matched once and then stopped — it probably uses a numeric AutomationId. See Find elements.
Last modified on October 14, 2026