An application's own automation interface
Some applications offer an object for scripts beside their windows: a COM automation interface, as many line-of-business
applications register one. Through it a test arranges data and checks results through the application's own model,
where the screen shows them poorly: the rows of a table, a document's body, a value deep in a form. app.automation()
reaches that object through SPART-Desk's helper executable, by COM late binding, and walks it along a path of members.
This page is for anyone whose application has such an interface.
import { Desktop, expectDesk } from "@spartaficial/spart-desk";
await using desktop = await Desktop.create();
const app = await desktop.launch({ executable: "C:/Apps/Orders/Orders.exe", args: ["C:\\Data\\orders.odb"] });
const file = app.automation({ document: "C:\\Data\\orders.odb" });
const orders = file.at('Tables("Orders")');
await orders.set('Rows(1).Cells("Customer").Value', "Acme Ltd");
expectDesk(await orders.get('Rows(1, 2).Values')).toEqual([
["Order 1042", 10.5],
["Order 1043", 21],
]);
await app.window().getByRole("button", { name: "Recalculate" }).click();
Sources
| Source | What it finds |
|---|---|
{ progId: "Orders.Application" } | The object the application registered in the Running Object Table under that ProgID, its application object. Found when the application registers it, within the call's timeout: an application registers it once its window is up. |
{ document: "C:\\Data\\orders.odb" } | The object holding a document the application has open, registered under the moniker of its full path, as such applications register each open document. A document that is not open is not opened: the call fails with automation_unavailable. |
Only the application's own object is used. The process serving the object must be one of the application's processes
(its own, or one showing its windows). An instance the user had open, or another application's, fails with
not_application, so a test never writes into someone's own document.
Paths
Members are joined by dots, each with its arguments in parentheses, as a macro language writes them:
Documents(1).Tables("Orders").Rows(1, 3).Values.
- Arguments are literals: numbers, strings in double quotes (
\"and\\inside),true,false,null. A path names what to reach; it does not compute. - Default members: a member that takes no arguments but is given some is got, and its default member is given
them.
Documents(1)isDocuments, then itsItem(1). - Each call walks the path again from the running object.
at(path)is a place further along it, so a place stays valid however the application changes, and nothing is held between calls.
Calls
| Call | What it does | Answers |
|---|---|---|
get(path, { timeout?, signal? }) | Gets the member the path ends at | its value |
set(path, value, { timeout?, signal? }) | Sets it | — |
call(path, args?, { timeout?, signal? }) | Calls the method the path ends at; an argument undefined is left out, as an optional one | what it answered |
Values (AutomationValue):
- text, numbers, booleans and
null; - dates as
Date, in local time, as COM keeps them; - arrays: a block of values is rows of values,
[["Order 1042", 10.5], ["Order 1043", 21]], whatever its lower bound; - an object answered as
{ kind: "object", type }(Table,Document). Walk to it with a longer path rather than passing it back.
Timeouts: 30 s by default (AUTOMATION_TIMEOUT_MS).
- A get that runs out fails as
timeout. - A set or a call that runs out fails as
delivery_unknown, since it may have been done, and is not sent again. - A server that does not answer is let go at the deadline. The helper stops calling it after four calls held that way
(
not_responding), until it answers. - A server that rejects calls while busy (a modal dialog open in the application) is asked again until the timeout.
Records: each call is in the action log (automation.get, automation.set, automation.call) with its source and
path, and what kind of value it read or set (text of 12 characters, 3 × 2 cells), never the values. A get is an
observation, so it leaves what the screen showed valid and is not pictured as a step.
Errors
BackendError code | Meaning |
|---|---|
automation_unavailable | No running object under that ProgID, or none holding that document, within the timeout; or the ProgID is not registered for the user |
not_application | Another process than the application's serves the object, or which one serves it cannot be told |
automation_failed | The object refused the call: an unknown member, a wrong argument, an error of its own. The message keeps the server's words and the HRESULT |
timeout, delivery_unknown | A get, or a set or a call, that ran out of time |
not_responding | Four calls are still held by a server that does not answer |
not_supported | A backend without the helper (a runtime of one's own) |
Where it runs
- Locally, and inside the Windows Sandbox guest (experimental) through its agent, as every helper request does.
- Through the agent only when its operator allows it. Calling any member of any server is code execution, so the
agent offers its
desktop_automationtool only for the interfaces named with--allow-automation <progId|document>(repeatable;allowAutomationin its policy), and for no other (the agent's README).