@phyto/driver-tauri 0.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +167 -0
- package/dist/index.js +287 -0
- package/dist/index.js.map +1 -0
- package/package.json +51 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Union of supported locator strategies. The same shapes the harness
|
|
3
|
+
* accepts on the wire — duplicated here so users of the driver don't
|
|
4
|
+
* have to depend on the `phyto` package.
|
|
5
|
+
*
|
|
6
|
+
* - `css`: standard CSS selector (the most flexible — also produced
|
|
7
|
+
* when a plain string is passed to any driver method).
|
|
8
|
+
* - `role`: ARIA role with optional accessible name (exact match on
|
|
9
|
+
* `textContent` or `aria-label`/`aria-labelledby`).
|
|
10
|
+
* - `text`: any element whose textContent includes the given string.
|
|
11
|
+
* - `testId`: shorthand for `[data-testid="..."]`.
|
|
12
|
+
* - `component`: framework-aware lookup (currently React only).
|
|
13
|
+
*/
|
|
14
|
+
type Locator = {
|
|
15
|
+
css: string;
|
|
16
|
+
} | {
|
|
17
|
+
role: string;
|
|
18
|
+
name?: string;
|
|
19
|
+
} | {
|
|
20
|
+
text: string;
|
|
21
|
+
} | {
|
|
22
|
+
testId: string;
|
|
23
|
+
} | {
|
|
24
|
+
component: string;
|
|
25
|
+
props?: Record<string, unknown>;
|
|
26
|
+
};
|
|
27
|
+
/** Argument shape accepted by element-targeting driver methods. */
|
|
28
|
+
type LocatorInput = string | Locator;
|
|
29
|
+
interface TauriDriverOptions {
|
|
30
|
+
/** Path to the built Tauri binary */
|
|
31
|
+
binary: string;
|
|
32
|
+
/** Port for the automation server (default: 9876) */
|
|
33
|
+
port?: number;
|
|
34
|
+
/** Timeout in ms for waitUntilReady polling (default: 30000) */
|
|
35
|
+
readyTimeout?: number;
|
|
36
|
+
/** Poll interval in ms for waitUntilReady (default: 250) */
|
|
37
|
+
readyInterval?: number;
|
|
38
|
+
/** Additional environment variables for the app process */
|
|
39
|
+
env?: Record<string, string>;
|
|
40
|
+
/** Default timeout in ms for all wait operations (default: 5000) */
|
|
41
|
+
defaultTimeout?: number;
|
|
42
|
+
}
|
|
43
|
+
/** Transport-level response wrapper from the Tauri automation server. */
|
|
44
|
+
interface TransportResult<T = unknown> {
|
|
45
|
+
ok: boolean;
|
|
46
|
+
value?: T;
|
|
47
|
+
error?: string;
|
|
48
|
+
}
|
|
49
|
+
interface WaitOptions {
|
|
50
|
+
/** Timeout in ms (default: driver's defaultTimeout) */
|
|
51
|
+
timeout?: number;
|
|
52
|
+
/** Poll interval in ms (default: 100) */
|
|
53
|
+
interval?: number;
|
|
54
|
+
}
|
|
55
|
+
interface InteractionOptions extends WaitOptions {
|
|
56
|
+
/** If true, also wait for the element to not be disabled (default: false) */
|
|
57
|
+
enabled?: boolean;
|
|
58
|
+
}
|
|
59
|
+
declare class TauriDriver {
|
|
60
|
+
private process;
|
|
61
|
+
private readonly binary;
|
|
62
|
+
private readonly port;
|
|
63
|
+
private readonly readyTimeout;
|
|
64
|
+
private readonly readyInterval;
|
|
65
|
+
private readonly env;
|
|
66
|
+
private readonly baseUrl;
|
|
67
|
+
private readonly defaultTimeout;
|
|
68
|
+
constructor(options: TauriDriverOptions);
|
|
69
|
+
/**
|
|
70
|
+
* Launch the Tauri app and wait until the automation server is ready.
|
|
71
|
+
*/
|
|
72
|
+
launch(): Promise<void>;
|
|
73
|
+
/**
|
|
74
|
+
* Poll the /health endpoint until the automation server is responsive,
|
|
75
|
+
* then verify wire-protocol compatibility via /info.
|
|
76
|
+
*
|
|
77
|
+
* Fails fast (without retry) on a version mismatch — re-polling won't
|
|
78
|
+
* fix a stale plugin binary.
|
|
79
|
+
*/
|
|
80
|
+
waitUntilReady(): Promise<void>;
|
|
81
|
+
/**
|
|
82
|
+
* Fetch plugin metadata (wire protocol + plugin version) from /info.
|
|
83
|
+
* Used by `waitUntilReady` to assert the plugin's protocol version
|
|
84
|
+
* matches this driver's `PROTOCOL_VERSION`.
|
|
85
|
+
*/
|
|
86
|
+
fetchInfo(): Promise<{
|
|
87
|
+
protocol_version: number;
|
|
88
|
+
plugin_version: string;
|
|
89
|
+
plugin: string;
|
|
90
|
+
}>;
|
|
91
|
+
/**
|
|
92
|
+
* Verify the running plugin's wire protocol matches this driver's.
|
|
93
|
+
* Throws a clear error on mismatch — both versions named so the user
|
|
94
|
+
* knows which side is stale.
|
|
95
|
+
*/
|
|
96
|
+
private assertPluginCompatible;
|
|
97
|
+
/**
|
|
98
|
+
* Shut down the app process.
|
|
99
|
+
*/
|
|
100
|
+
shutdown(): Promise<void>;
|
|
101
|
+
/**
|
|
102
|
+
* Evaluate arbitrary JavaScript in the webview and return the result.
|
|
103
|
+
* This is an escape hatch for operations not covered by the command
|
|
104
|
+
* protocol (e.g. custom app-specific scripts).
|
|
105
|
+
*/
|
|
106
|
+
evaluate<T = unknown>(script: string): Promise<T>;
|
|
107
|
+
/**
|
|
108
|
+
* Send a declarative command to the in-page harness via POST /command.
|
|
109
|
+
* Returns the structured result from the harness.
|
|
110
|
+
*
|
|
111
|
+
* Throws if the harness returns `{ ok: false }`.
|
|
112
|
+
*/
|
|
113
|
+
command<T = unknown>(payload: Record<string, unknown>): Promise<T>;
|
|
114
|
+
/**
|
|
115
|
+
* Perform the protocol version handshake with the in-page harness.
|
|
116
|
+
* Returns true if the harness is available and compatible.
|
|
117
|
+
*/
|
|
118
|
+
handshake(): Promise<void>;
|
|
119
|
+
/**
|
|
120
|
+
* Click an element matching the given target. Accepts a CSS selector
|
|
121
|
+
* string or any `Locator` object (e.g. `{ role: "button", name: "Next" }`
|
|
122
|
+
* for text-based button matching, or `{ testId: "..." }`).
|
|
123
|
+
* Auto-waits for the element to be visible before clicking.
|
|
124
|
+
* With `{ enabled: true }`, also waits for `!el.disabled` — useful for
|
|
125
|
+
* buttons gated by form validation.
|
|
126
|
+
*/
|
|
127
|
+
click(target: LocatorInput, options?: InteractionOptions): Promise<void>;
|
|
128
|
+
/**
|
|
129
|
+
* Type text into an input/textarea matching the given target.
|
|
130
|
+
* Auto-waits for the element to be visible before typing.
|
|
131
|
+
* Finds the actual `<input>` or `<textarea>` element (even inside wrapper
|
|
132
|
+
* components — useful for Mantine, which puts `data-testid` on the
|
|
133
|
+
* wrapper `<div>`), sets the value using the React-compatible native
|
|
134
|
+
* setter, and dispatches input/change events so React's synthetic
|
|
135
|
+
* event system fires onChange handlers.
|
|
136
|
+
*/
|
|
137
|
+
type(target: LocatorInput, text: string, options?: InteractionOptions): Promise<void>;
|
|
138
|
+
/**
|
|
139
|
+
* Get the text content of an element matching the given target.
|
|
140
|
+
* Auto-waits for the element to be visible before reading.
|
|
141
|
+
*/
|
|
142
|
+
textContent(target: LocatorInput, options?: InteractionOptions): Promise<string>;
|
|
143
|
+
/**
|
|
144
|
+
* Wait until an element matching the target is in the DOM and visible.
|
|
145
|
+
*/
|
|
146
|
+
waitFor(target: LocatorInput, options?: WaitOptions): Promise<void>;
|
|
147
|
+
/**
|
|
148
|
+
* Wait until an element matching the target contains the specified text.
|
|
149
|
+
*/
|
|
150
|
+
waitForText(target: LocatorInput, text: string, options?: WaitOptions): Promise<void>;
|
|
151
|
+
/**
|
|
152
|
+
* Wait until any of the given targets matches a visible element.
|
|
153
|
+
* Returns the target that matched, so the caller can branch.
|
|
154
|
+
*/
|
|
155
|
+
waitForAny<T extends LocatorInput>(targets: T[], options?: WaitOptions): Promise<T>;
|
|
156
|
+
/**
|
|
157
|
+
* Wait until an element is no longer in the DOM.
|
|
158
|
+
*/
|
|
159
|
+
waitForGone(target: LocatorInput, options?: WaitOptions): Promise<void>;
|
|
160
|
+
/**
|
|
161
|
+
* Invoke a Tauri command from the webview.
|
|
162
|
+
* Calls the Tauri backend directly via window.__TAURI_INTERNALS__.invoke().
|
|
163
|
+
*/
|
|
164
|
+
invoke<T = unknown>(command: string, args?: Record<string, unknown>): Promise<T>;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export { type InteractionOptions, type Locator, type LocatorInput, TauriDriver, type TauriDriverOptions, type TransportResult, type WaitOptions };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import { spawn } from "child_process";
|
|
3
|
+
var PROTOCOL_VERSION = 1;
|
|
4
|
+
function toLocator(input) {
|
|
5
|
+
return typeof input === "string" ? { css: input } : input;
|
|
6
|
+
}
|
|
7
|
+
var TauriDriver = class {
|
|
8
|
+
process = null;
|
|
9
|
+
binary;
|
|
10
|
+
port;
|
|
11
|
+
readyTimeout;
|
|
12
|
+
readyInterval;
|
|
13
|
+
env;
|
|
14
|
+
baseUrl;
|
|
15
|
+
defaultTimeout;
|
|
16
|
+
constructor(options) {
|
|
17
|
+
this.binary = options.binary;
|
|
18
|
+
this.port = options.port ?? 9876;
|
|
19
|
+
this.readyTimeout = options.readyTimeout ?? 12e4;
|
|
20
|
+
this.readyInterval = options.readyInterval ?? 250;
|
|
21
|
+
this.env = options.env ?? {};
|
|
22
|
+
this.baseUrl = `http://localhost:${this.port}`;
|
|
23
|
+
this.defaultTimeout = options.defaultTimeout ?? 5e3;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Launch the Tauri app and wait until the automation server is ready.
|
|
27
|
+
*/
|
|
28
|
+
async launch() {
|
|
29
|
+
this.process = spawn(this.binary, [], {
|
|
30
|
+
stdio: "pipe",
|
|
31
|
+
env: { ...process.env, ...this.env }
|
|
32
|
+
});
|
|
33
|
+
this.process.stderr?.on("data", (data) => {
|
|
34
|
+
const line = data.toString().trim();
|
|
35
|
+
if (line) {
|
|
36
|
+
process.stderr.write(`[app] ${line}
|
|
37
|
+
`);
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
this.process.on("error", (err) => {
|
|
41
|
+
throw new Error(`Failed to launch app binary: ${err.message}`);
|
|
42
|
+
});
|
|
43
|
+
await this.waitUntilReady();
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Poll the /health endpoint until the automation server is responsive,
|
|
47
|
+
* then verify wire-protocol compatibility via /info.
|
|
48
|
+
*
|
|
49
|
+
* Fails fast (without retry) on a version mismatch — re-polling won't
|
|
50
|
+
* fix a stale plugin binary.
|
|
51
|
+
*/
|
|
52
|
+
async waitUntilReady() {
|
|
53
|
+
const deadline = Date.now() + this.readyTimeout;
|
|
54
|
+
while (Date.now() < deadline) {
|
|
55
|
+
try {
|
|
56
|
+
const res = await fetch(`${this.baseUrl}/health`, {
|
|
57
|
+
signal: AbortSignal.timeout(2e3)
|
|
58
|
+
});
|
|
59
|
+
if (res.ok) {
|
|
60
|
+
await this.assertPluginCompatible();
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
} catch {
|
|
64
|
+
}
|
|
65
|
+
await sleep(this.readyInterval);
|
|
66
|
+
}
|
|
67
|
+
throw new Error(
|
|
68
|
+
`Automation server did not become ready within ${this.readyTimeout}ms`
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Fetch plugin metadata (wire protocol + plugin version) from /info.
|
|
73
|
+
* Used by `waitUntilReady` to assert the plugin's protocol version
|
|
74
|
+
* matches this driver's `PROTOCOL_VERSION`.
|
|
75
|
+
*/
|
|
76
|
+
async fetchInfo() {
|
|
77
|
+
const res = await fetch(`${this.baseUrl}/info`, {
|
|
78
|
+
signal: AbortSignal.timeout(2e3)
|
|
79
|
+
});
|
|
80
|
+
if (!res.ok) {
|
|
81
|
+
throw new Error(
|
|
82
|
+
`GET /info failed with status ${res.status}. Is tauri-plugin-phyto installed and registered?`
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
return await res.json();
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Verify the running plugin's wire protocol matches this driver's.
|
|
89
|
+
* Throws a clear error on mismatch — both versions named so the user
|
|
90
|
+
* knows which side is stale.
|
|
91
|
+
*/
|
|
92
|
+
async assertPluginCompatible() {
|
|
93
|
+
const info = await this.fetchInfo();
|
|
94
|
+
if (info.protocol_version !== PROTOCOL_VERSION) {
|
|
95
|
+
throw new Error(
|
|
96
|
+
`Phyto protocol version mismatch:
|
|
97
|
+
- @phyto/driver-tauri: wire protocol v${PROTOCOL_VERSION}
|
|
98
|
+
- tauri-plugin-phyto (v${info.plugin_version}): wire protocol v${info.protocol_version}
|
|
99
|
+
|
|
100
|
+
Re-build the Tauri app against a matching version of tauri-plugin-phyto, or update the npm packages to match the plugin.`
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Shut down the app process.
|
|
106
|
+
*/
|
|
107
|
+
async shutdown() {
|
|
108
|
+
if (this.process) {
|
|
109
|
+
this.process.kill("SIGTERM");
|
|
110
|
+
await Promise.race([
|
|
111
|
+
new Promise((resolve) => {
|
|
112
|
+
this.process?.on("exit", () => resolve());
|
|
113
|
+
}),
|
|
114
|
+
sleep(5e3).then(() => {
|
|
115
|
+
this.process?.kill("SIGKILL");
|
|
116
|
+
})
|
|
117
|
+
]);
|
|
118
|
+
this.process = null;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Evaluate arbitrary JavaScript in the webview and return the result.
|
|
123
|
+
* This is an escape hatch for operations not covered by the command
|
|
124
|
+
* protocol (e.g. custom app-specific scripts).
|
|
125
|
+
*/
|
|
126
|
+
async evaluate(script) {
|
|
127
|
+
return this.command({
|
|
128
|
+
action: "eval",
|
|
129
|
+
script
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Send a declarative command to the in-page harness via POST /command.
|
|
134
|
+
* Returns the structured result from the harness.
|
|
135
|
+
*
|
|
136
|
+
* Throws if the harness returns `{ ok: false }`.
|
|
137
|
+
*/
|
|
138
|
+
async command(payload) {
|
|
139
|
+
const res = await fetch(`${this.baseUrl}/command`, {
|
|
140
|
+
method: "POST",
|
|
141
|
+
headers: { "Content-Type": "application/json" },
|
|
142
|
+
body: JSON.stringify(payload)
|
|
143
|
+
});
|
|
144
|
+
const result = await res.json();
|
|
145
|
+
if (!result.ok) {
|
|
146
|
+
throw new Error(result.error ?? "Command transport failed");
|
|
147
|
+
}
|
|
148
|
+
const commandResult = result.value;
|
|
149
|
+
if (!commandResult || !commandResult.ok) {
|
|
150
|
+
throw new Error(
|
|
151
|
+
commandResult?.error ?? "Command failed with unknown error"
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
return commandResult.value;
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Perform the protocol version handshake with the in-page harness.
|
|
158
|
+
* Returns true if the harness is available and compatible.
|
|
159
|
+
*/
|
|
160
|
+
async handshake() {
|
|
161
|
+
await this.command({
|
|
162
|
+
action: "handshake",
|
|
163
|
+
protocolVersion: PROTOCOL_VERSION
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
// ─── Public API ─────────────────────────────────────────────────
|
|
167
|
+
/**
|
|
168
|
+
* Click an element matching the given target. Accepts a CSS selector
|
|
169
|
+
* string or any `Locator` object (e.g. `{ role: "button", name: "Next" }`
|
|
170
|
+
* for text-based button matching, or `{ testId: "..." }`).
|
|
171
|
+
* Auto-waits for the element to be visible before clicking.
|
|
172
|
+
* With `{ enabled: true }`, also waits for `!el.disabled` — useful for
|
|
173
|
+
* buttons gated by form validation.
|
|
174
|
+
*/
|
|
175
|
+
async click(target, options) {
|
|
176
|
+
await this.command({
|
|
177
|
+
action: "click",
|
|
178
|
+
locator: toLocator(target),
|
|
179
|
+
options: {
|
|
180
|
+
timeout: options?.timeout ?? this.defaultTimeout,
|
|
181
|
+
enabled: options?.enabled
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Type text into an input/textarea matching the given target.
|
|
187
|
+
* Auto-waits for the element to be visible before typing.
|
|
188
|
+
* Finds the actual `<input>` or `<textarea>` element (even inside wrapper
|
|
189
|
+
* components — useful for Mantine, which puts `data-testid` on the
|
|
190
|
+
* wrapper `<div>`), sets the value using the React-compatible native
|
|
191
|
+
* setter, and dispatches input/change events so React's synthetic
|
|
192
|
+
* event system fires onChange handlers.
|
|
193
|
+
*/
|
|
194
|
+
async type(target, text, options) {
|
|
195
|
+
await this.command({
|
|
196
|
+
action: "type",
|
|
197
|
+
locator: toLocator(target),
|
|
198
|
+
text,
|
|
199
|
+
options: {
|
|
200
|
+
timeout: options?.timeout ?? this.defaultTimeout
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Get the text content of an element matching the given target.
|
|
206
|
+
* Auto-waits for the element to be visible before reading.
|
|
207
|
+
*/
|
|
208
|
+
async textContent(target, options) {
|
|
209
|
+
return this.command({
|
|
210
|
+
action: "text-content",
|
|
211
|
+
locator: toLocator(target),
|
|
212
|
+
options: {
|
|
213
|
+
timeout: options?.timeout ?? this.defaultTimeout
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Wait until an element matching the target is in the DOM and visible.
|
|
219
|
+
*/
|
|
220
|
+
async waitFor(target, options) {
|
|
221
|
+
await this.command({
|
|
222
|
+
action: "wait-for",
|
|
223
|
+
locator: toLocator(target),
|
|
224
|
+
options: {
|
|
225
|
+
timeout: options?.timeout ?? this.defaultTimeout
|
|
226
|
+
}
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Wait until an element matching the target contains the specified text.
|
|
231
|
+
*/
|
|
232
|
+
async waitForText(target, text, options) {
|
|
233
|
+
await this.command({
|
|
234
|
+
action: "wait-for-text",
|
|
235
|
+
locator: toLocator(target),
|
|
236
|
+
text,
|
|
237
|
+
options: {
|
|
238
|
+
timeout: options?.timeout ?? this.defaultTimeout
|
|
239
|
+
}
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* Wait until any of the given targets matches a visible element.
|
|
244
|
+
* Returns the target that matched, so the caller can branch.
|
|
245
|
+
*/
|
|
246
|
+
async waitForAny(targets, options) {
|
|
247
|
+
const locators = targets.map(toLocator);
|
|
248
|
+
const matchedIndex = await this.command({
|
|
249
|
+
action: "wait-for-any",
|
|
250
|
+
locators,
|
|
251
|
+
options: {
|
|
252
|
+
timeout: options?.timeout ?? this.defaultTimeout
|
|
253
|
+
}
|
|
254
|
+
});
|
|
255
|
+
return targets[matchedIndex];
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* Wait until an element is no longer in the DOM.
|
|
259
|
+
*/
|
|
260
|
+
async waitForGone(target, options) {
|
|
261
|
+
await this.command({
|
|
262
|
+
action: "wait-for-gone",
|
|
263
|
+
locator: toLocator(target),
|
|
264
|
+
options: {
|
|
265
|
+
timeout: options?.timeout ?? this.defaultTimeout
|
|
266
|
+
}
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* Invoke a Tauri command from the webview.
|
|
271
|
+
* Calls the Tauri backend directly via window.__TAURI_INTERNALS__.invoke().
|
|
272
|
+
*/
|
|
273
|
+
async invoke(command, args) {
|
|
274
|
+
return this.command({
|
|
275
|
+
action: "invoke",
|
|
276
|
+
command,
|
|
277
|
+
args: args ?? {}
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
};
|
|
281
|
+
function sleep(ms) {
|
|
282
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
283
|
+
}
|
|
284
|
+
export {
|
|
285
|
+
TauriDriver
|
|
286
|
+
};
|
|
287
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["import { ChildProcess, spawn } from \"node:child_process\";\n\n/** Protocol version — must match the harness. */\nconst PROTOCOL_VERSION = 1;\n\n// ─── Locator types ──────────────────────────────────────────────────\n\n/**\n * Union of supported locator strategies. The same shapes the harness\n * accepts on the wire — duplicated here so users of the driver don't\n * have to depend on the `phyto` package.\n *\n * - `css`: standard CSS selector (the most flexible — also produced\n * when a plain string is passed to any driver method).\n * - `role`: ARIA role with optional accessible name (exact match on\n * `textContent` or `aria-label`/`aria-labelledby`).\n * - `text`: any element whose textContent includes the given string.\n * - `testId`: shorthand for `[data-testid=\"...\"]`.\n * - `component`: framework-aware lookup (currently React only).\n */\nexport type Locator =\n | { css: string }\n | { role: string; name?: string }\n | { text: string }\n | { testId: string }\n | { component: string; props?: Record<string, unknown> };\n\n/** Argument shape accepted by element-targeting driver methods. */\nexport type LocatorInput = string | Locator;\n\n/**\n * Normalize a `LocatorInput` to a `Locator`. A bare string is treated\n * as a CSS selector — preserves backward compatibility with the\n * original string-only API.\n */\nfunction toLocator(input: LocatorInput): Locator {\n return typeof input === \"string\" ? { css: input } : input;\n}\n\n// ─── Types ──────────────────────────────────────────────────────────\n\nexport interface TauriDriverOptions {\n /** Path to the built Tauri binary */\n binary: string;\n /** Port for the automation server (default: 9876) */\n port?: number;\n /** Timeout in ms for waitUntilReady polling (default: 30000) */\n readyTimeout?: number;\n /** Poll interval in ms for waitUntilReady (default: 250) */\n readyInterval?: number;\n /** Additional environment variables for the app process */\n env?: Record<string, string>;\n /** Default timeout in ms for all wait operations (default: 5000) */\n defaultTimeout?: number;\n}\n\n/** Transport-level response wrapper from the Tauri automation server. */\nexport interface TransportResult<T = unknown> {\n ok: boolean;\n value?: T;\n error?: string;\n}\n\nexport interface WaitOptions {\n /** Timeout in ms (default: driver's defaultTimeout) */\n timeout?: number;\n /** Poll interval in ms (default: 100) */\n interval?: number;\n}\n\nexport interface InteractionOptions extends WaitOptions {\n /** If true, also wait for the element to not be disabled (default: false) */\n enabled?: boolean;\n}\n\n// ─── Driver ─────────────────────────────────────────────────────────\n\nexport class TauriDriver {\n private process: ChildProcess | null = null;\n private readonly binary: string;\n private readonly port: number;\n private readonly readyTimeout: number;\n private readonly readyInterval: number;\n private readonly env: Record<string, string>;\n private readonly baseUrl: string;\n private readonly defaultTimeout: number;\n\n constructor(options: TauriDriverOptions) {\n this.binary = options.binary;\n this.port = options.port ?? 9876;\n this.readyTimeout = options.readyTimeout ?? 120_000;\n this.readyInterval = options.readyInterval ?? 250;\n this.env = options.env ?? {};\n this.baseUrl = `http://localhost:${this.port}`;\n this.defaultTimeout = options.defaultTimeout ?? 5000;\n }\n\n /**\n * Launch the Tauri app and wait until the automation server is ready.\n */\n async launch(): Promise<void> {\n this.process = spawn(this.binary, [], {\n stdio: \"pipe\",\n env: { ...process.env, ...this.env },\n });\n\n // Forward stderr for debugging\n this.process.stderr?.on(\"data\", (data: Buffer) => {\n const line = data.toString().trim();\n if (line) {\n process.stderr.write(`[app] ${line}\\n`);\n }\n });\n\n this.process.on(\"error\", (err) => {\n throw new Error(`Failed to launch app binary: ${err.message}`);\n });\n\n await this.waitUntilReady();\n }\n\n /**\n * Poll the /health endpoint until the automation server is responsive,\n * then verify wire-protocol compatibility via /info.\n *\n * Fails fast (without retry) on a version mismatch — re-polling won't\n * fix a stale plugin binary.\n */\n async waitUntilReady(): Promise<void> {\n const deadline = Date.now() + this.readyTimeout;\n\n while (Date.now() < deadline) {\n try {\n const res = await fetch(`${this.baseUrl}/health`, {\n signal: AbortSignal.timeout(2000),\n });\n if (res.ok) {\n await this.assertPluginCompatible();\n return;\n }\n } catch {\n // Server not ready yet — keep polling\n }\n await sleep(this.readyInterval);\n }\n\n throw new Error(\n `Automation server did not become ready within ${this.readyTimeout}ms`\n );\n }\n\n /**\n * Fetch plugin metadata (wire protocol + plugin version) from /info.\n * Used by `waitUntilReady` to assert the plugin's protocol version\n * matches this driver's `PROTOCOL_VERSION`.\n */\n async fetchInfo(): Promise<{\n protocol_version: number;\n plugin_version: string;\n plugin: string;\n }> {\n const res = await fetch(`${this.baseUrl}/info`, {\n signal: AbortSignal.timeout(2000),\n });\n if (!res.ok) {\n throw new Error(\n `GET /info failed with status ${res.status}. Is tauri-plugin-phyto installed and registered?`\n );\n }\n return (await res.json()) as {\n protocol_version: number;\n plugin_version: string;\n plugin: string;\n };\n }\n\n /**\n * Verify the running plugin's wire protocol matches this driver's.\n * Throws a clear error on mismatch — both versions named so the user\n * knows which side is stale.\n */\n private async assertPluginCompatible(): Promise<void> {\n const info = await this.fetchInfo();\n if (info.protocol_version !== PROTOCOL_VERSION) {\n throw new Error(\n `Phyto protocol version mismatch:\\n` +\n ` - @phyto/driver-tauri: wire protocol v${PROTOCOL_VERSION}\\n` +\n ` - tauri-plugin-phyto (v${info.plugin_version}): wire protocol v${info.protocol_version}\\n` +\n `\\n` +\n `Re-build the Tauri app against a matching version of tauri-plugin-phyto, ` +\n `or update the npm packages to match the plugin.`\n );\n }\n }\n\n /**\n * Shut down the app process.\n */\n async shutdown(): Promise<void> {\n if (this.process) {\n this.process.kill(\"SIGTERM\");\n\n // Give it a moment to exit gracefully, then force kill\n await Promise.race([\n new Promise<void>((resolve) => {\n this.process?.on(\"exit\", () => resolve());\n }),\n sleep(5000).then(() => {\n this.process?.kill(\"SIGKILL\");\n }),\n ]);\n\n this.process = null;\n }\n }\n\n /**\n * Evaluate arbitrary JavaScript in the webview and return the result.\n * This is an escape hatch for operations not covered by the command\n * protocol (e.g. custom app-specific scripts).\n */\n async evaluate<T = unknown>(script: string): Promise<T> {\n return this.command<T>({\n action: \"eval\",\n script,\n });\n }\n\n /**\n * Send a declarative command to the in-page harness via POST /command.\n * Returns the structured result from the harness.\n *\n * Throws if the harness returns `{ ok: false }`.\n */\n async command<T = unknown>(payload: Record<string, unknown>): Promise<T> {\n const res = await fetch(`${this.baseUrl}/command`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify(payload),\n });\n\n const result: TransportResult<{ ok: boolean; value?: T; error?: string }> =\n await res.json();\n\n // The /command endpoint wraps the harness result in a TransportResult envelope.\n // result.ok = true means the eval succeeded (harness didn't throw).\n // result.value contains the CommandResult from the harness.\n if (!result.ok) {\n throw new Error(result.error ?? \"Command transport failed\");\n }\n\n const commandResult = result.value;\n if (!commandResult || !commandResult.ok) {\n throw new Error(\n commandResult?.error ?? \"Command failed with unknown error\"\n );\n }\n\n return commandResult.value as T;\n }\n\n /**\n * Perform the protocol version handshake with the in-page harness.\n * Returns true if the harness is available and compatible.\n */\n async handshake(): Promise<void> {\n await this.command({\n action: \"handshake\",\n protocolVersion: PROTOCOL_VERSION,\n });\n }\n\n // ─── Public API ─────────────────────────────────────────────────\n\n /**\n * Click an element matching the given target. Accepts a CSS selector\n * string or any `Locator` object (e.g. `{ role: \"button\", name: \"Next\" }`\n * for text-based button matching, or `{ testId: \"...\" }`).\n * Auto-waits for the element to be visible before clicking.\n * With `{ enabled: true }`, also waits for `!el.disabled` — useful for\n * buttons gated by form validation.\n */\n async click(target: LocatorInput, options?: InteractionOptions): Promise<void> {\n await this.command({\n action: \"click\",\n locator: toLocator(target),\n options: {\n timeout: options?.timeout ?? this.defaultTimeout,\n enabled: options?.enabled,\n },\n });\n }\n\n /**\n * Type text into an input/textarea matching the given target.\n * Auto-waits for the element to be visible before typing.\n * Finds the actual `<input>` or `<textarea>` element (even inside wrapper\n * components — useful for Mantine, which puts `data-testid` on the\n * wrapper `<div>`), sets the value using the React-compatible native\n * setter, and dispatches input/change events so React's synthetic\n * event system fires onChange handlers.\n */\n async type(\n target: LocatorInput,\n text: string,\n options?: InteractionOptions\n ): Promise<void> {\n await this.command({\n action: \"type\",\n locator: toLocator(target),\n text,\n options: {\n timeout: options?.timeout ?? this.defaultTimeout,\n },\n });\n }\n\n /**\n * Get the text content of an element matching the given target.\n * Auto-waits for the element to be visible before reading.\n */\n async textContent(\n target: LocatorInput,\n options?: InteractionOptions\n ): Promise<string> {\n return this.command<string>({\n action: \"text-content\",\n locator: toLocator(target),\n options: {\n timeout: options?.timeout ?? this.defaultTimeout,\n },\n });\n }\n\n /**\n * Wait until an element matching the target is in the DOM and visible.\n */\n async waitFor(target: LocatorInput, options?: WaitOptions): Promise<void> {\n await this.command({\n action: \"wait-for\",\n locator: toLocator(target),\n options: {\n timeout: options?.timeout ?? this.defaultTimeout,\n },\n });\n }\n\n /**\n * Wait until an element matching the target contains the specified text.\n */\n async waitForText(\n target: LocatorInput,\n text: string,\n options?: WaitOptions\n ): Promise<void> {\n await this.command({\n action: \"wait-for-text\",\n locator: toLocator(target),\n text,\n options: {\n timeout: options?.timeout ?? this.defaultTimeout,\n },\n });\n }\n\n /**\n * Wait until any of the given targets matches a visible element.\n * Returns the target that matched, so the caller can branch.\n */\n async waitForAny<T extends LocatorInput>(\n targets: T[],\n options?: WaitOptions\n ): Promise<T> {\n const locators = targets.map(toLocator);\n const matchedIndex = await this.command<number>({\n action: \"wait-for-any\",\n locators,\n options: {\n timeout: options?.timeout ?? this.defaultTimeout,\n },\n });\n return targets[matchedIndex];\n }\n\n /**\n * Wait until an element is no longer in the DOM.\n */\n async waitForGone(\n target: LocatorInput,\n options?: WaitOptions\n ): Promise<void> {\n await this.command({\n action: \"wait-for-gone\",\n locator: toLocator(target),\n options: {\n timeout: options?.timeout ?? this.defaultTimeout,\n },\n });\n }\n\n /**\n * Invoke a Tauri command from the webview.\n * Calls the Tauri backend directly via window.__TAURI_INTERNALS__.invoke().\n */\n async invoke<T = unknown>(\n command: string,\n args?: Record<string, unknown>\n ): Promise<T> {\n return this.command<T>({\n action: \"invoke\",\n command,\n args: args ?? {},\n });\n }\n}\n\n// ─── Helpers ────────────────────────────────────────────────────────\n\nfunction sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n"],"mappings":"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","names":[]}
|
package/package.json
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@phyto/driver-tauri",
|
|
3
|
+
"version": "0.1.5",
|
|
4
|
+
"description": "Tauri driver for Phyto — controls a running Tauri app via the tauri-plugin-phyto automation socket.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "Coniferous <hello@coniferous.dev>",
|
|
7
|
+
"homepage": "https://github.com/coniferous-dev/phyto#readme",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/coniferous-dev/phyto.git",
|
|
11
|
+
"directory": "packages/driver-tauri"
|
|
12
|
+
},
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/coniferous-dev/phyto/issues"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"phyto",
|
|
18
|
+
"tauri",
|
|
19
|
+
"driver",
|
|
20
|
+
"testing",
|
|
21
|
+
"e2e",
|
|
22
|
+
"automation"
|
|
23
|
+
],
|
|
24
|
+
"type": "module",
|
|
25
|
+
"exports": {
|
|
26
|
+
".": {
|
|
27
|
+
"import": "./dist/index.js",
|
|
28
|
+
"types": "./dist/index.d.ts"
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
"main": "./dist/index.js",
|
|
32
|
+
"types": "./dist/index.d.ts",
|
|
33
|
+
"files": [
|
|
34
|
+
"dist",
|
|
35
|
+
"README.md",
|
|
36
|
+
"LICENSE"
|
|
37
|
+
],
|
|
38
|
+
"scripts": {
|
|
39
|
+
"build": "tsup",
|
|
40
|
+
"dev": "tsup --watch",
|
|
41
|
+
"typecheck": "tsc --noEmit",
|
|
42
|
+
"prepublishOnly": "npm run build"
|
|
43
|
+
},
|
|
44
|
+
"devDependencies": {
|
|
45
|
+
"tsup": "^8.4.0",
|
|
46
|
+
"typescript": "~5.7.2"
|
|
47
|
+
},
|
|
48
|
+
"publishConfig": {
|
|
49
|
+
"access": "public"
|
|
50
|
+
}
|
|
51
|
+
}
|