@autifyhq/aximo-cli 0.7.0 → 0.9.0
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/README.md +28 -13
- package/dist/client.d.ts +7 -1
- package/dist/client.js +21 -2
- package/dist/client.test.js +36 -0
- package/dist/commands/case/case.test.js +62 -0
- package/dist/commands/case/create.d.ts +1 -1
- package/dist/commands/case/create.js +4 -4
- package/dist/commands/case/index.js +1 -0
- package/dist/commands/case/update.d.ts +50 -0
- package/dist/commands/case/update.js +78 -0
- package/dist/commands/health/check.js +15 -3
- package/dist/commands/health/check.test.js +27 -3
- package/dist/commands/mobile/connect-core.d.ts +3 -1
- package/dist/commands/mobile/connect-core.js +34 -14
- package/dist/commands/mobile/connect.d.ts +5 -0
- package/dist/commands/mobile/connect.js +10 -4
- package/dist/commands/mobile/connect.test.js +7 -1
- package/dist/commands/mobile/doctor.d.ts +1 -0
- package/dist/commands/mobile/doctor.js +18 -2
- package/dist/commands/mobile/doctor.test.js +15 -1
- package/dist/commands/mobile/install.d.ts +1 -0
- package/dist/commands/mobile/install.js +14 -3
- package/dist/commands/mobile/list.d.ts +1 -0
- package/dist/commands/mobile/list.js +23 -3
- package/dist/commands/mobile/list.test.d.ts +1 -0
- package/dist/commands/mobile/list.test.js +24 -0
- package/dist/commands/mobile/output.d.ts +13 -0
- package/dist/commands/mobile/output.js +21 -0
- package/dist/commands/mobile/output.test.d.ts +1 -0
- package/dist/commands/mobile/output.test.js +54 -0
- package/dist/commands/mobile/register.d.ts +1 -0
- package/dist/commands/mobile/register.js +11 -3
- package/dist/commands/mobile/setup.d.ts +1 -0
- package/dist/commands/mobile/setup.js +7 -2
- package/dist/commands/mobile/signing-consent.d.ts +18 -0
- package/dist/commands/mobile/signing-consent.js +28 -0
- package/dist/commands/mobile/signing-consent.test.d.ts +1 -0
- package/dist/commands/mobile/signing-consent.test.js +50 -0
- package/dist/commands/mobile/unregister.d.ts +7 -3
- package/dist/commands/mobile/unregister.js +10 -4
- package/dist/commands/plan/index.js +1 -0
- package/dist/commands/plan/plan.test.js +61 -0
- package/dist/commands/plan/update.d.ts +42 -0
- package/dist/commands/plan/update.js +78 -0
- package/dist/commands/schema.js +1 -1
- package/dist/commands/session/save-as-case.js +3 -3
- package/dist/commands/trigger/plan/run.d.ts +6 -1
- package/dist/commands/trigger/plan/run.js +9 -3
- package/dist/commands/trigger/trigger.test.js +22 -3
- package/dist/commands/upload/put.d.ts +5 -0
- package/dist/commands/upload/put.js +44 -11
- package/dist/commands/upload/upload.test.js +82 -0
- package/dist/mobile-control/appium-controller.d.ts +4 -0
- package/dist/mobile-control/appium-controller.js +90 -2
- package/dist/mobile-control/appium-controller.test.js +156 -8
- package/dist/mobile-control/bundle-id-map.js +3 -0
- package/dist/mobile-control/controller.d.ts +3 -0
- package/dist/mobile-control/exec.d.ts +5 -1
- package/dist/mobile-control/exec.js +2 -2
- package/dist/mobile-control/ios-tunnel.d.ts +5 -2
- package/dist/mobile-control/ios-tunnel.js +25 -5
- package/dist/mobile-control/ios-tunnel.test.js +8 -3
- package/dist/mobile-control/ios-wda-preinstall.js +6 -3
- package/dist/mobile-control/ios-wda-preinstall.test.js +12 -8
- package/dist/mobile-control/tunnel.js +57 -1
- package/dist/mobile-control/tunnel.test.js +157 -0
- package/dist/mobile-control/webdriver.d.ts +7 -1
- package/dist/mobile-control/webdriver.js +12 -6
- package/dist/mutate.d.ts +1 -1
- package/dist/mutate.js +4 -2
- package/dist/types.d.ts +49 -4
- package/dist/validate.d.ts +6 -1
- package/dist/validate.js +46 -0
- package/dist/validate.test.js +39 -1
- package/package.json +1 -1
|
@@ -49,10 +49,43 @@ function defaultConnect(wsUrl, deviceToken) {
|
|
|
49
49
|
timeout: 10000,
|
|
50
50
|
});
|
|
51
51
|
}
|
|
52
|
+
// Mirrors the gateway's lock lease: a session quiet for longer than this has lost
|
|
53
|
+
// its server-side lock, so a new session id is a legitimate takeover, not a stray.
|
|
54
|
+
const SESSION_LEASE_MS = 25 * 60 * 1000;
|
|
52
55
|
export function registerHandlers(socket, controller, installApp) {
|
|
56
|
+
// Device-side guard against interleaved sessions: the gateway announces owner
|
|
57
|
+
// changes (session_acquired/session_released) and tags each command with its
|
|
58
|
+
// session id, so commands from a session that no longer owns the device are
|
|
59
|
+
// dropped even if a gateway instance lost its in-memory lock.
|
|
60
|
+
let activeSession = null;
|
|
61
|
+
socket.on("session_acquired", (args) => {
|
|
62
|
+
const sessionId = args?.sessionId;
|
|
63
|
+
if (typeof sessionId === "string" && sessionId) {
|
|
64
|
+
activeSession = { id: sessionId, lastSeenAt: Date.now() };
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
socket.on("session_released", (args) => {
|
|
68
|
+
// Only the owner's release clears the guard; a takeover by a new session is
|
|
69
|
+
// announced via session_acquired, so an unmatched release is ignored.
|
|
70
|
+
const sessionId = args?.sessionId;
|
|
71
|
+
if (activeSession && activeSession.id === sessionId) {
|
|
72
|
+
activeSession = null;
|
|
73
|
+
}
|
|
74
|
+
});
|
|
53
75
|
const on = (event, handler) => {
|
|
54
76
|
socket.on(event, (rawArgs, ack) => {
|
|
55
|
-
|
|
77
|
+
// Destructure the routing-only tag off a clone so the caller's payload
|
|
78
|
+
// object is never mutated.
|
|
79
|
+
const { __sessionId: sessionId, ...args } = (rawArgs ?? {});
|
|
80
|
+
if (typeof sessionId === "string" && sessionId) {
|
|
81
|
+
if (activeSession &&
|
|
82
|
+
activeSession.id !== sessionId &&
|
|
83
|
+
Date.now() - activeSession.lastSeenAt < SESSION_LEASE_MS) {
|
|
84
|
+
ack?.(fail(new Error("Device is busy with another session")));
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
activeSession = { id: sessionId, lastSeenAt: Date.now() };
|
|
88
|
+
}
|
|
56
89
|
void handler(args)
|
|
57
90
|
.then((result) => ack?.(result))
|
|
58
91
|
.catch((err) => ack?.(fail(err)));
|
|
@@ -97,6 +130,29 @@ export function registerHandlers(socket, controller, installApp) {
|
|
|
97
130
|
await controller.swipe(r.coords[0], r.coords[1], r.coords[2], r.coords[3], num(args["duration"], 300));
|
|
98
131
|
return ok();
|
|
99
132
|
});
|
|
133
|
+
on("pinch_zoom", async (args) => {
|
|
134
|
+
const r = requireCoords(args, ["x", "y"]);
|
|
135
|
+
if ("error" in r)
|
|
136
|
+
return { ok: false, error: `pinch_zoom ${r.error}` };
|
|
137
|
+
const scale = reqNum(args, "scale");
|
|
138
|
+
if (scale === null)
|
|
139
|
+
return { ok: false, error: "pinch_zoom requires a numeric scale" };
|
|
140
|
+
await controller.pinchZoom(r.coords[0], r.coords[1], scale, num(args["duration"], 600));
|
|
141
|
+
return ok();
|
|
142
|
+
});
|
|
143
|
+
on("rotate_device", async (args) => {
|
|
144
|
+
const orientation = String(args["orientation"] ?? "").toLowerCase();
|
|
145
|
+
if (orientation !== "portrait" && orientation !== "landscape") {
|
|
146
|
+
return { ok: false, error: 'rotate_device requires orientation "portrait" or "landscape"' };
|
|
147
|
+
}
|
|
148
|
+
return asActionResult(await controller.rotateDevice(orientation));
|
|
149
|
+
});
|
|
150
|
+
on("set_location", async (args) => {
|
|
151
|
+
const r = requireCoords(args, ["latitude", "longitude"]);
|
|
152
|
+
if ("error" in r)
|
|
153
|
+
return { ok: false, error: `set_location ${r.error}` };
|
|
154
|
+
return asActionResult(await controller.setLocation(r.coords[0], r.coords[1], num(args["altitude"], 0)));
|
|
155
|
+
});
|
|
100
156
|
on("input", async (args) => asActionResult(await controller.typeText(String(args["text"] ?? ""))));
|
|
101
157
|
on("key", async (args) => asActionResult(await controller.pressKey(String(args["key"] ?? ""))));
|
|
102
158
|
on("launch_app", async (args) => asActionResult(await controller.launchApp(String(args["app"] ?? ""))));
|
|
@@ -20,12 +20,15 @@ function stubController(overrides = {}) {
|
|
|
20
20
|
doubleTap: vi.fn(async () => { }),
|
|
21
21
|
longPress: vi.fn(async () => { }),
|
|
22
22
|
swipe: vi.fn(async () => { }),
|
|
23
|
+
pinchZoom: vi.fn(async () => { }),
|
|
23
24
|
typeText: vi.fn(async () => ({ ok: true })),
|
|
24
25
|
pressKey: vi.fn(async () => ({ ok: true })),
|
|
25
26
|
launchApp: vi.fn(async () => ({ ok: true })),
|
|
26
27
|
findElement: vi.fn(async () => ({ ok: true })),
|
|
27
28
|
clearText: vi.fn(async () => ({ ok: true })),
|
|
28
29
|
hideKeyboard: vi.fn(async () => ({ ok: true })),
|
|
30
|
+
rotateDevice: vi.fn(async () => ({ ok: true })),
|
|
31
|
+
setLocation: vi.fn(async () => ({ ok: true })),
|
|
29
32
|
close: vi.fn(async () => { }),
|
|
30
33
|
};
|
|
31
34
|
return { ...base, ...overrides };
|
|
@@ -116,6 +119,78 @@ describe("registerHandlers", () => {
|
|
|
116
119
|
error: "no focused field",
|
|
117
120
|
});
|
|
118
121
|
});
|
|
122
|
+
it("pinch_zoom forwards center, scale, and duration", async () => {
|
|
123
|
+
const pinchZoom = vi.fn(async () => { });
|
|
124
|
+
const socket = wire(stubController({ pinchZoom }));
|
|
125
|
+
await expect(socket.emit("pinch_zoom", { x: 100, y: 200, scale: 2, duration: 800 })).resolves.toEqual({
|
|
126
|
+
ok: true,
|
|
127
|
+
});
|
|
128
|
+
expect(pinchZoom).toHaveBeenCalledWith(100, 200, 2, 800);
|
|
129
|
+
});
|
|
130
|
+
it("pinch_zoom defaults duration to 600ms", async () => {
|
|
131
|
+
const pinchZoom = vi.fn(async () => { });
|
|
132
|
+
const socket = wire(stubController({ pinchZoom }));
|
|
133
|
+
await socket.emit("pinch_zoom", { x: 1, y: 2, scale: 0.5 });
|
|
134
|
+
expect(pinchZoom).toHaveBeenCalledWith(1, 2, 0.5, 600);
|
|
135
|
+
});
|
|
136
|
+
it("pinch_zoom rejects a missing scale without calling the controller", async () => {
|
|
137
|
+
const pinchZoom = vi.fn(async () => { });
|
|
138
|
+
const socket = wire(stubController({ pinchZoom }));
|
|
139
|
+
await expect(socket.emit("pinch_zoom", { x: 1, y: 2 })).resolves.toEqual({
|
|
140
|
+
ok: false,
|
|
141
|
+
error: "pinch_zoom requires a numeric scale",
|
|
142
|
+
});
|
|
143
|
+
expect(pinchZoom).not.toHaveBeenCalled();
|
|
144
|
+
});
|
|
145
|
+
it("pinch_zoom rejects missing coordinates", async () => {
|
|
146
|
+
const pinchZoom = vi.fn(async () => { });
|
|
147
|
+
const socket = wire(stubController({ pinchZoom }));
|
|
148
|
+
await expect(socket.emit("pinch_zoom", { scale: 2 })).resolves.toEqual({
|
|
149
|
+
ok: false,
|
|
150
|
+
error: "pinch_zoom requires numeric coordinates",
|
|
151
|
+
});
|
|
152
|
+
expect(pinchZoom).not.toHaveBeenCalled();
|
|
153
|
+
});
|
|
154
|
+
it("rotate_device forwards a valid orientation", async () => {
|
|
155
|
+
const rotateDevice = vi.fn(async () => ({ ok: true }));
|
|
156
|
+
const socket = wire(stubController({ rotateDevice }));
|
|
157
|
+
await expect(socket.emit("rotate_device", { orientation: "landscape" })).resolves.toEqual({
|
|
158
|
+
ok: true,
|
|
159
|
+
});
|
|
160
|
+
expect(rotateDevice).toHaveBeenCalledWith("landscape");
|
|
161
|
+
});
|
|
162
|
+
it("rotate_device rejects an invalid orientation without calling the controller", async () => {
|
|
163
|
+
const rotateDevice = vi.fn(async () => ({ ok: true }));
|
|
164
|
+
const socket = wire(stubController({ rotateDevice }));
|
|
165
|
+
await expect(socket.emit("rotate_device", { orientation: "sideways" })).resolves.toEqual({
|
|
166
|
+
ok: false,
|
|
167
|
+
error: 'rotate_device requires orientation "portrait" or "landscape"',
|
|
168
|
+
});
|
|
169
|
+
expect(rotateDevice).not.toHaveBeenCalled();
|
|
170
|
+
});
|
|
171
|
+
it("set_location forwards coordinates with altitude defaulting to 0", async () => {
|
|
172
|
+
const setLocation = vi.fn(async () => ({ ok: true }));
|
|
173
|
+
const socket = wire(stubController({ setLocation }));
|
|
174
|
+
await expect(socket.emit("set_location", { latitude: 35.68, longitude: 139.69 })).resolves.toEqual({
|
|
175
|
+
ok: true,
|
|
176
|
+
});
|
|
177
|
+
expect(setLocation).toHaveBeenCalledWith(35.68, 139.69, 0);
|
|
178
|
+
});
|
|
179
|
+
it("set_location rejects a non-numeric latitude without calling the controller", async () => {
|
|
180
|
+
const setLocation = vi.fn(async () => ({ ok: true }));
|
|
181
|
+
const socket = wire(stubController({ setLocation }));
|
|
182
|
+
await expect(socket.emit("set_location", { latitude: "35", longitude: 139.69 })).resolves.toEqual({
|
|
183
|
+
ok: false,
|
|
184
|
+
error: "set_location requires numeric coordinates",
|
|
185
|
+
});
|
|
186
|
+
expect(setLocation).not.toHaveBeenCalled();
|
|
187
|
+
});
|
|
188
|
+
it("set_location acks the structured failure from the controller", async () => {
|
|
189
|
+
const setLocation = vi.fn(async () => ({ ok: false, error: "mock location not allowed" }));
|
|
190
|
+
const socket = wire(stubController({ setLocation }));
|
|
191
|
+
await expect(socket.emit("set_location", { latitude: 1, longitude: 2, altitude: 30 })).resolves.toEqual({ ok: false, error: "mock location not allowed" });
|
|
192
|
+
expect(setLocation).toHaveBeenCalledWith(1, 2, 30);
|
|
193
|
+
});
|
|
119
194
|
it("hide_keyboard acks ok", async () => {
|
|
120
195
|
const hideKeyboard = vi.fn(async () => ({ ok: true }));
|
|
121
196
|
const socket = wire(stubController({ hideKeyboard }));
|
|
@@ -153,3 +228,85 @@ describe("registerHandlers", () => {
|
|
|
153
228
|
});
|
|
154
229
|
});
|
|
155
230
|
});
|
|
231
|
+
describe("registerHandlers session guard", () => {
|
|
232
|
+
const BUSY = { ok: false, error: "Device is busy with another session" };
|
|
233
|
+
function fire(socket, event, args) {
|
|
234
|
+
socket.handlers.get(event)?.(args);
|
|
235
|
+
}
|
|
236
|
+
it("strips __sessionId for the handler without mutating the incoming payload", async () => {
|
|
237
|
+
const tap = vi.fn(async () => { });
|
|
238
|
+
const socket = wire(stubController({ tap }));
|
|
239
|
+
const payload = { x: 5, y: 9, __sessionId: "s1" };
|
|
240
|
+
await expect(socket.emit("tap", payload)).resolves.toEqual({ ok: true });
|
|
241
|
+
expect(tap).toHaveBeenCalledWith(5, 9);
|
|
242
|
+
expect(payload.__sessionId).toBe("s1");
|
|
243
|
+
});
|
|
244
|
+
it("rejects a second session's command while the first is active", async () => {
|
|
245
|
+
const tap = vi.fn(async () => { });
|
|
246
|
+
const socket = wire(stubController({ tap }));
|
|
247
|
+
await socket.emit("tap", { x: 1, y: 2, __sessionId: "s1" });
|
|
248
|
+
await expect(socket.emit("tap", { x: 3, y: 4, __sessionId: "s2" })).resolves.toEqual(BUSY);
|
|
249
|
+
expect(tap).toHaveBeenCalledTimes(1);
|
|
250
|
+
});
|
|
251
|
+
it("accepts a new session after session_released for the active one", async () => {
|
|
252
|
+
const tap = vi.fn(async () => { });
|
|
253
|
+
const socket = wire(stubController({ tap }));
|
|
254
|
+
await socket.emit("tap", { x: 1, y: 2, __sessionId: "s1" });
|
|
255
|
+
fire(socket, "session_released", { sessionId: "s1" });
|
|
256
|
+
await expect(socket.emit("tap", { x: 3, y: 4, __sessionId: "s2" })).resolves.toEqual({
|
|
257
|
+
ok: true,
|
|
258
|
+
});
|
|
259
|
+
expect(tap).toHaveBeenCalledTimes(2);
|
|
260
|
+
});
|
|
261
|
+
it("ignores session_released for a session that is not active", async () => {
|
|
262
|
+
const tap = vi.fn(async () => { });
|
|
263
|
+
const socket = wire(stubController({ tap }));
|
|
264
|
+
await socket.emit("tap", { x: 1, y: 2, __sessionId: "s1" });
|
|
265
|
+
fire(socket, "session_released", { sessionId: "other" });
|
|
266
|
+
await expect(socket.emit("tap", { x: 3, y: 4, __sessionId: "s2" })).resolves.toEqual(BUSY);
|
|
267
|
+
expect(tap).toHaveBeenCalledTimes(1);
|
|
268
|
+
});
|
|
269
|
+
it("ignores session_released without a sessionId (malformed release)", async () => {
|
|
270
|
+
const tap = vi.fn(async () => { });
|
|
271
|
+
const socket = wire(stubController({ tap }));
|
|
272
|
+
await socket.emit("tap", { x: 1, y: 2, __sessionId: "s1" });
|
|
273
|
+
fire(socket, "session_released", {});
|
|
274
|
+
fire(socket, "session_released", { sessionId: null });
|
|
275
|
+
await expect(socket.emit("tap", { x: 3, y: 4, __sessionId: "s2" })).resolves.toEqual(BUSY);
|
|
276
|
+
expect(tap).toHaveBeenCalledTimes(1);
|
|
277
|
+
});
|
|
278
|
+
it("switches owner on session_acquired and rejects the superseded session", async () => {
|
|
279
|
+
const tap = vi.fn(async () => { });
|
|
280
|
+
const socket = wire(stubController({ tap }));
|
|
281
|
+
await socket.emit("tap", { x: 1, y: 2, __sessionId: "s1" });
|
|
282
|
+
fire(socket, "session_acquired", { sessionId: "s2" });
|
|
283
|
+
await expect(socket.emit("tap", { x: 3, y: 4, __sessionId: "s1" })).resolves.toEqual(BUSY);
|
|
284
|
+
await expect(socket.emit("tap", { x: 5, y: 6, __sessionId: "s2" })).resolves.toEqual({
|
|
285
|
+
ok: true,
|
|
286
|
+
});
|
|
287
|
+
expect(tap).toHaveBeenCalledTimes(2);
|
|
288
|
+
});
|
|
289
|
+
it("allows takeover once the active session is idle past the lease", async () => {
|
|
290
|
+
vi.useFakeTimers();
|
|
291
|
+
try {
|
|
292
|
+
const tap = vi.fn(async () => { });
|
|
293
|
+
const socket = wire(stubController({ tap }));
|
|
294
|
+
await socket.emit("tap", { x: 1, y: 2, __sessionId: "s1" });
|
|
295
|
+
vi.advanceTimersByTime(26 * 60 * 1000);
|
|
296
|
+
await expect(socket.emit("tap", { x: 3, y: 4, __sessionId: "s2" })).resolves.toEqual({
|
|
297
|
+
ok: true,
|
|
298
|
+
});
|
|
299
|
+
expect(tap).toHaveBeenCalledTimes(2);
|
|
300
|
+
}
|
|
301
|
+
finally {
|
|
302
|
+
vi.useRealTimers();
|
|
303
|
+
}
|
|
304
|
+
});
|
|
305
|
+
it("leaves untagged commands ungated (pre-tagging gateways)", async () => {
|
|
306
|
+
const tap = vi.fn(async () => { });
|
|
307
|
+
const socket = wire(stubController({ tap }));
|
|
308
|
+
await socket.emit("tap", { x: 1, y: 2, __sessionId: "s1" });
|
|
309
|
+
await expect(socket.emit("tap", { x: 3, y: 4 })).resolves.toEqual({ ok: true });
|
|
310
|
+
expect(tap).toHaveBeenCalledTimes(2);
|
|
311
|
+
});
|
|
312
|
+
});
|
|
@@ -18,6 +18,12 @@ export declare class WebDriverSession {
|
|
|
18
18
|
private requireId;
|
|
19
19
|
screenshot(): Promise<Buffer>;
|
|
20
20
|
windowRect(): Promise<WindowRect>;
|
|
21
|
+
setOrientation(orientation: "PORTRAIT" | "LANDSCAPE"): Promise<void>;
|
|
22
|
+
setGeoLocation(location: {
|
|
23
|
+
latitude: number;
|
|
24
|
+
longitude: number;
|
|
25
|
+
altitude: number;
|
|
26
|
+
}): Promise<void>;
|
|
21
27
|
performActions(actions: unknown[]): Promise<void>;
|
|
22
28
|
releaseActions(): Promise<void>;
|
|
23
29
|
getActiveElement(): Promise<string | null>;
|
|
@@ -25,7 +31,7 @@ export declare class WebDriverSession {
|
|
|
25
31
|
elementClick(elementRef: string): Promise<void>;
|
|
26
32
|
elementClear(elementRef: string): Promise<void>;
|
|
27
33
|
elementSendKeys(elementRef: string, text: string): Promise<void>;
|
|
34
|
+
elementDisplayed(elementRef: string): Promise<boolean>;
|
|
28
35
|
execute(script: string, args?: unknown[]): Promise<unknown>;
|
|
29
|
-
setValueOnActiveElement(text: string): Promise<void>;
|
|
30
36
|
}
|
|
31
37
|
export declare const ELEMENT_KEY = "element-6066-11e4-a52e-4f735466cecf";
|
|
@@ -84,6 +84,14 @@ export class WebDriverSession {
|
|
|
84
84
|
y: Number(value?.y ?? 0),
|
|
85
85
|
};
|
|
86
86
|
}
|
|
87
|
+
async setOrientation(orientation) {
|
|
88
|
+
await this.transport.request("POST", `/session/${this.requireId()}/orientation`, {
|
|
89
|
+
orientation,
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
async setGeoLocation(location) {
|
|
93
|
+
await this.transport.request("POST", `/session/${this.requireId()}/location`, { location });
|
|
94
|
+
}
|
|
87
95
|
async performActions(actions) {
|
|
88
96
|
await this.transport.request("POST", `/session/${this.requireId()}/actions`, { actions });
|
|
89
97
|
}
|
|
@@ -110,18 +118,16 @@ export class WebDriverSession {
|
|
|
110
118
|
async elementSendKeys(elementRef, text) {
|
|
111
119
|
await this.transport.request("POST", `/session/${this.requireId()}/element/${elementRef}/value`, { text, value: [...text] });
|
|
112
120
|
}
|
|
121
|
+
async elementDisplayed(elementRef) {
|
|
122
|
+
const value = await this.transport.request("GET", `/session/${this.requireId()}/element/${elementRef}/displayed`);
|
|
123
|
+
return value === true;
|
|
124
|
+
}
|
|
113
125
|
async execute(script, args = []) {
|
|
114
126
|
return this.transport.request("POST", `/session/${this.requireId()}/execute/sync`, {
|
|
115
127
|
script,
|
|
116
128
|
args,
|
|
117
129
|
});
|
|
118
130
|
}
|
|
119
|
-
async setValueOnActiveElement(text) {
|
|
120
|
-
const active = await this.getActiveElement();
|
|
121
|
-
if (!active)
|
|
122
|
-
throw new Error("No active element to receive text");
|
|
123
|
-
await this.elementSendKeys(active, text);
|
|
124
|
-
}
|
|
125
131
|
}
|
|
126
132
|
export const ELEMENT_KEY = "element-6066-11e4-a52e-4f735466cecf";
|
|
127
133
|
function elementId(value) {
|
package/dist/mutate.d.ts
CHANGED
|
@@ -5,7 +5,7 @@ export interface DryRunResult {
|
|
|
5
5
|
url: string;
|
|
6
6
|
body?: unknown;
|
|
7
7
|
}
|
|
8
|
-
export declare function dryRunEnvelope(method: string,
|
|
8
|
+
export declare function dryRunEnvelope(method: string, pathOrUrl: string, body?: unknown): SuccessEnvelope<DryRunResult>;
|
|
9
9
|
export declare function resolveJsonOverride(jsonFlag: unknown): {
|
|
10
10
|
body: Record<string, unknown>;
|
|
11
11
|
} | {
|
package/dist/mutate.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { formatError, formatSuccess, validationError } from "./output.js";
|
|
2
2
|
import { parseJsonBody, JsonBodyError, OPENAPI_URL } from "./validate.js";
|
|
3
3
|
import { getBaseUrl } from "./client.js";
|
|
4
|
-
export function dryRunEnvelope(method,
|
|
5
|
-
const url =
|
|
4
|
+
export function dryRunEnvelope(method, pathOrUrl, body) {
|
|
5
|
+
const url = /^https?:\/\//.test(pathOrUrl)
|
|
6
|
+
? pathOrUrl
|
|
7
|
+
: `${getBaseUrl()}${pathOrUrl.startsWith("/") ? pathOrUrl : `/${pathOrUrl}`}`;
|
|
6
8
|
const data = { dryRun: true, method, url };
|
|
7
9
|
if (body !== undefined)
|
|
8
10
|
data.body = body;
|
package/dist/types.d.ts
CHANGED
|
@@ -19,8 +19,12 @@ export interface ErrorEnvelope {
|
|
|
19
19
|
export type Envelope<T> = SuccessEnvelope<T> | ErrorEnvelope;
|
|
20
20
|
export type Platform = "ANDROID" | "IOS";
|
|
21
21
|
export type FileType = "ANDROID_APP" | "IOS_APP";
|
|
22
|
+
export type ExecutionMode = "parallel" | "sequential";
|
|
23
|
+
export type CaseType = "web" | "mobile" | "desktop" | "multi_device";
|
|
24
|
+
export type CaseFormat = "gherkin" | "markdown" | "plain_text";
|
|
22
25
|
export interface HealthStatus {
|
|
23
26
|
status: string;
|
|
27
|
+
auth?: string | undefined;
|
|
24
28
|
}
|
|
25
29
|
export interface CreateSessionInput {
|
|
26
30
|
title: string;
|
|
@@ -75,11 +79,30 @@ export interface CaseRunStatus {
|
|
|
75
79
|
export interface PlanRunInput {
|
|
76
80
|
model?: string | undefined;
|
|
77
81
|
mobileConfig?: Record<string, unknown> | undefined;
|
|
82
|
+
executionMode?: ExecutionMode | undefined;
|
|
78
83
|
}
|
|
79
84
|
export interface PlanRunResult {
|
|
80
85
|
success: boolean;
|
|
81
86
|
planRunId: string;
|
|
82
87
|
totalCases: number;
|
|
88
|
+
executionMode?: ExecutionMode | undefined;
|
|
89
|
+
}
|
|
90
|
+
export interface UpdatePlanInput {
|
|
91
|
+
title?: string | undefined;
|
|
92
|
+
description?: string | undefined;
|
|
93
|
+
executionMode?: ExecutionMode | undefined;
|
|
94
|
+
stopOnFailure?: boolean | undefined;
|
|
95
|
+
}
|
|
96
|
+
export interface UpdatePlanResult {
|
|
97
|
+
success: boolean;
|
|
98
|
+
plan: {
|
|
99
|
+
id: string;
|
|
100
|
+
title: string;
|
|
101
|
+
description: string | null;
|
|
102
|
+
executionMode: ExecutionMode;
|
|
103
|
+
stopOnFailure: boolean;
|
|
104
|
+
updatedAt: string;
|
|
105
|
+
};
|
|
83
106
|
}
|
|
84
107
|
export interface PlanRun {
|
|
85
108
|
id: string;
|
|
@@ -123,10 +146,10 @@ export interface Project {
|
|
|
123
146
|
export interface CreateCaseInput {
|
|
124
147
|
projectId: string;
|
|
125
148
|
scenario: string;
|
|
126
|
-
type?:
|
|
149
|
+
type?: CaseType | undefined;
|
|
127
150
|
title?: string | undefined;
|
|
128
151
|
description?: string | undefined;
|
|
129
|
-
format?:
|
|
152
|
+
format?: CaseFormat | undefined;
|
|
130
153
|
startUrl?: string | undefined;
|
|
131
154
|
directoryId?: string | undefined;
|
|
132
155
|
}
|
|
@@ -134,6 +157,28 @@ export interface CreateCaseResult {
|
|
|
134
157
|
success: boolean;
|
|
135
158
|
caseId: string;
|
|
136
159
|
}
|
|
160
|
+
export interface UpdateCaseInput {
|
|
161
|
+
title?: string | undefined;
|
|
162
|
+
description?: string | undefined;
|
|
163
|
+
scenario?: string | undefined;
|
|
164
|
+
type?: CaseType | undefined;
|
|
165
|
+
format?: CaseFormat | undefined;
|
|
166
|
+
startUrl?: string | undefined;
|
|
167
|
+
}
|
|
168
|
+
export interface UpdateCaseResult {
|
|
169
|
+
success: boolean;
|
|
170
|
+
case: {
|
|
171
|
+
id: string;
|
|
172
|
+
projectId: string;
|
|
173
|
+
type: string;
|
|
174
|
+
title: string;
|
|
175
|
+
description: string | null;
|
|
176
|
+
scenario: string;
|
|
177
|
+
format: string;
|
|
178
|
+
startUrl: string | null;
|
|
179
|
+
updatedAt: string;
|
|
180
|
+
};
|
|
181
|
+
}
|
|
137
182
|
export interface DeleteCaseResult {
|
|
138
183
|
success: boolean;
|
|
139
184
|
}
|
|
@@ -151,9 +196,9 @@ export interface PlanCaseResult {
|
|
|
151
196
|
}
|
|
152
197
|
export interface SaveSessionAsCaseInput {
|
|
153
198
|
title?: string | undefined;
|
|
154
|
-
type?:
|
|
199
|
+
type?: Exclude<CaseType, "multi_device"> | undefined;
|
|
155
200
|
scenario?: string | undefined;
|
|
156
|
-
format?:
|
|
201
|
+
format?: CaseFormat | undefined;
|
|
157
202
|
}
|
|
158
203
|
export interface SaveSessionAsCaseResult {
|
|
159
204
|
success: boolean;
|
package/dist/validate.d.ts
CHANGED
|
@@ -1,10 +1,15 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { CaseFormat, CaseType, ExecutionMode, FileType, Platform } from "./types.js";
|
|
2
2
|
export declare function optionalString(name: string, value: unknown): string | undefined;
|
|
3
3
|
export declare function parseStringList(name: string, value: unknown): string[] | undefined;
|
|
4
4
|
export declare function requireString(name: string, value: unknown): string;
|
|
5
5
|
export declare function requireId(name: string, value: unknown): string;
|
|
6
6
|
export declare function parsePlatform(value: unknown): Platform | undefined;
|
|
7
7
|
export declare function parseFileType(value: unknown): FileType | undefined;
|
|
8
|
+
export declare function parseExecutionMode(value: unknown): ExecutionMode | undefined;
|
|
9
|
+
export declare function parseCaseType(value: unknown): CaseType | undefined;
|
|
10
|
+
export declare function parseSaveAsCaseType(value: unknown): Exclude<CaseType, "multi_device"> | undefined;
|
|
11
|
+
export declare function parseCaseFormat(value: unknown): CaseFormat | undefined;
|
|
12
|
+
export declare function parseOptionalBoolean(name: string, value: unknown): boolean | undefined;
|
|
8
13
|
export declare function parseTags(value: unknown): string[] | undefined;
|
|
9
14
|
export declare const OPENAPI_URL = "https://aximo.autify.com/openapi.yaml";
|
|
10
15
|
export declare class JsonBodyError extends Error {
|
package/dist/validate.js
CHANGED
|
@@ -56,6 +56,52 @@ export function parseFileType(value) {
|
|
|
56
56
|
}
|
|
57
57
|
return value;
|
|
58
58
|
}
|
|
59
|
+
const EXECUTION_MODES = new Set(["parallel", "sequential"]);
|
|
60
|
+
export function parseExecutionMode(value) {
|
|
61
|
+
if (value === undefined || value === "")
|
|
62
|
+
return undefined;
|
|
63
|
+
if (typeof value !== "string" || !EXECUTION_MODES.has(value)) {
|
|
64
|
+
throw new Error(`executionMode must be parallel or sequential (got ${String(value)})`);
|
|
65
|
+
}
|
|
66
|
+
return value;
|
|
67
|
+
}
|
|
68
|
+
const CASE_TYPES = new Set(["web", "mobile", "desktop", "multi_device"]);
|
|
69
|
+
export function parseCaseType(value) {
|
|
70
|
+
if (value === undefined || value === "")
|
|
71
|
+
return undefined;
|
|
72
|
+
if (typeof value !== "string" || !CASE_TYPES.has(value)) {
|
|
73
|
+
throw new Error(`type must be web, mobile, desktop, or multi_device (got ${String(value)})`);
|
|
74
|
+
}
|
|
75
|
+
return value;
|
|
76
|
+
}
|
|
77
|
+
// save-as-case's server schema accepts a narrower set than case create/update.
|
|
78
|
+
export function parseSaveAsCaseType(value) {
|
|
79
|
+
const parsed = parseCaseType(value);
|
|
80
|
+
if (parsed === "multi_device") {
|
|
81
|
+
throw new Error(`type must be web, mobile, or desktop (got ${String(value)})`);
|
|
82
|
+
}
|
|
83
|
+
return parsed;
|
|
84
|
+
}
|
|
85
|
+
const CASE_FORMATS = new Set(["gherkin", "markdown", "plain_text"]);
|
|
86
|
+
export function parseCaseFormat(value) {
|
|
87
|
+
if (value === undefined || value === "")
|
|
88
|
+
return undefined;
|
|
89
|
+
if (typeof value !== "string" || !CASE_FORMATS.has(value)) {
|
|
90
|
+
throw new Error(`format must be gherkin, markdown, or plain_text (got ${String(value)})`);
|
|
91
|
+
}
|
|
92
|
+
return value;
|
|
93
|
+
}
|
|
94
|
+
// Tri-state boolean for PATCH-style flags: absent means "leave unchanged",
|
|
95
|
+
// so a citty boolean (which defaults to false) would silently overwrite.
|
|
96
|
+
export function parseOptionalBoolean(name, value) {
|
|
97
|
+
if (value === undefined || value === "")
|
|
98
|
+
return undefined;
|
|
99
|
+
if (value === true || value === "true")
|
|
100
|
+
return true;
|
|
101
|
+
if (value === false || value === "false")
|
|
102
|
+
return false;
|
|
103
|
+
throw new Error(`${name} must be true or false (got ${String(value)})`);
|
|
104
|
+
}
|
|
59
105
|
export function parseTags(value) {
|
|
60
106
|
return parseStringList("tags", value);
|
|
61
107
|
}
|
package/dist/validate.test.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, expect, it } from "vitest";
|
|
2
|
-
import { requireString, requireId, parsePlatform, parseFileType, parseTags, parseJsonObject, parseJsonBody, JsonBodyError, } from "./validate.js";
|
|
2
|
+
import { requireString, requireId, parseCaseFormat, parseCaseType, parsePlatform, parseSaveAsCaseType, parseFileType, parseTags, parseJsonObject, parseJsonBody, JsonBodyError, } from "./validate.js";
|
|
3
3
|
describe("requireId", () => {
|
|
4
4
|
it("accepts plain IDs", () => {
|
|
5
5
|
expect(requireId("sessionId", "abc123_-")).toBe("abc123_-");
|
|
@@ -94,3 +94,41 @@ describe("parseJsonObject", () => {
|
|
|
94
94
|
expect(parseJsonObject("x", "")).toBeUndefined();
|
|
95
95
|
});
|
|
96
96
|
});
|
|
97
|
+
describe("parseCaseType", () => {
|
|
98
|
+
it("accepts all case types", () => {
|
|
99
|
+
expect(parseCaseType("web")).toBe("web");
|
|
100
|
+
expect(parseCaseType("mobile")).toBe("mobile");
|
|
101
|
+
expect(parseCaseType("desktop")).toBe("desktop");
|
|
102
|
+
expect(parseCaseType("multi_device")).toBe("multi_device");
|
|
103
|
+
});
|
|
104
|
+
it("returns undefined for absent values", () => {
|
|
105
|
+
expect(parseCaseType(undefined)).toBeUndefined();
|
|
106
|
+
expect(parseCaseType("")).toBeUndefined();
|
|
107
|
+
});
|
|
108
|
+
it("rejects unknown values", () => {
|
|
109
|
+
expect(() => parseCaseType("banana")).toThrow(/must be web, mobile, desktop/);
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
describe("parseSaveAsCaseType", () => {
|
|
113
|
+
it("accepts the narrower save-as-case set", () => {
|
|
114
|
+
expect(parseSaveAsCaseType("web")).toBe("web");
|
|
115
|
+
expect(parseSaveAsCaseType("desktop")).toBe("desktop");
|
|
116
|
+
});
|
|
117
|
+
it("rejects multi_device, which the save-as-case endpoint does not accept", () => {
|
|
118
|
+
expect(() => parseSaveAsCaseType("multi_device")).toThrow(/must be web, mobile, or desktop/);
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
describe("parseCaseFormat", () => {
|
|
122
|
+
it("accepts all formats", () => {
|
|
123
|
+
expect(parseCaseFormat("gherkin")).toBe("gherkin");
|
|
124
|
+
expect(parseCaseFormat("markdown")).toBe("markdown");
|
|
125
|
+
expect(parseCaseFormat("plain_text")).toBe("plain_text");
|
|
126
|
+
});
|
|
127
|
+
it("returns undefined for absent values", () => {
|
|
128
|
+
expect(parseCaseFormat(undefined)).toBeUndefined();
|
|
129
|
+
expect(parseCaseFormat("")).toBeUndefined();
|
|
130
|
+
});
|
|
131
|
+
it("rejects unknown values", () => {
|
|
132
|
+
expect(() => parseCaseFormat("klingon")).toThrow(/must be gherkin, markdown, or plain_text/);
|
|
133
|
+
});
|
|
134
|
+
});
|