@wdio/mcp 3.4.3 → 3.5.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 +38 -0
- package/lib/server.js +322 -18
- package/lib/server.js.map +1 -1
- package/lib/show-trace.d.ts +1 -0
- package/lib/show-trace.js +68 -0
- package/lib/show-trace.js.map +1 -0
- package/lib/trace.d.ts +89 -0
- package/lib/trace.js +110 -0
- package/lib/trace.js.map +1 -0
- package/package.json +13 -4
package/README.md
CHANGED
|
@@ -797,6 +797,44 @@ The generated script reconstructs the full session — including capabilities, n
|
|
|
797
797
|
standalone `import { remote } from 'webdriverio'` file. For BrowserStack sessions it includes the full try/catch/finally
|
|
798
798
|
with automatic session result marking.
|
|
799
799
|
|
|
800
|
+
### Trace Recording
|
|
801
|
+
|
|
802
|
+
Passing `trace: true` to `start_session` produces a Playwright-compatible `.trace` zip in the `.trace/` directory when
|
|
803
|
+
the session closes. The zip is playable at [player.vibium.dev](https://player.vibium.dev) and shows a filmstrip of
|
|
804
|
+
screenshots alongside the action timeline.
|
|
805
|
+
|
|
806
|
+
**How screenshots are timed**
|
|
807
|
+
|
|
808
|
+
Appium's `takeScreenshot` round-trip takes 700–1300 ms on a local emulator, which is long enough for the previous
|
|
809
|
+
action's animations to settle. We exploit this: each screenshot is captured **before** the next action fires, so what
|
|
810
|
+
the Appium server returns is already the settled result of the prior action.
|
|
811
|
+
|
|
812
|
+
The tricky part is making the trace player show that screenshot under the right action. The player associates a
|
|
813
|
+
`screencast-frame` event with whichever action's time window contains the frame's `timestamp` field. If the timestamp
|
|
814
|
+
is set to "now" (capture time), it falls before the current action's `startTime` and the player labels it as the
|
|
815
|
+
*before* state of the next action — one action out of sync.
|
|
816
|
+
|
|
817
|
+
The fix: stamp each `screencast-frame` with `lastAfterEndTime` — the `endTime` of the action that just completed. That
|
|
818
|
+
places the frame inside the previous action's window, so the player shows it as the result of that action, not the
|
|
819
|
+
precursor to the next one.
|
|
820
|
+
|
|
821
|
+
```
|
|
822
|
+
Timeline (monotonic ms):
|
|
823
|
+
|
|
824
|
+
prev.endTime ← frame timestamp stamped here
|
|
825
|
+
│
|
|
826
|
+
│ [screenshot captured here — shows settled state after prev action]
|
|
827
|
+
│
|
|
828
|
+
curr.startTime
|
|
829
|
+
│
|
|
830
|
+
│ [action executes]
|
|
831
|
+
│
|
|
832
|
+
curr.endTime ← next frame will be stamped here
|
|
833
|
+
```
|
|
834
|
+
|
|
835
|
+
The final screenshot at session close is stamped with the last action's `endTime`, so it renders under that action
|
|
836
|
+
rather than appearing as an orphaned frame after the timeline ends.
|
|
837
|
+
|
|
800
838
|
## Troubleshooting
|
|
801
839
|
|
|
802
840
|
**Browser automation not working?**
|
package/lib/server.js
CHANGED
|
@@ -46,7 +46,7 @@ var package_default = {
|
|
|
46
46
|
type: "git",
|
|
47
47
|
url: "git://github.com/webdriverio/mcp.git"
|
|
48
48
|
},
|
|
49
|
-
version: "3.4.
|
|
49
|
+
version: "3.4.4",
|
|
50
50
|
description: "MCP server with WebdriverIO for browser and mobile app automation (iOS/Android via Appium)",
|
|
51
51
|
main: "./lib/server.js",
|
|
52
52
|
module: "./lib/server.js",
|
|
@@ -59,10 +59,15 @@ var package_default = {
|
|
|
59
59
|
"./snapshot": {
|
|
60
60
|
import: "./lib/snapshot.js",
|
|
61
61
|
types: "./lib/snapshot.d.ts"
|
|
62
|
+
},
|
|
63
|
+
"./trace": {
|
|
64
|
+
import: "./lib/trace.js",
|
|
65
|
+
types: "./lib/trace.d.ts"
|
|
62
66
|
}
|
|
63
67
|
},
|
|
64
68
|
bin: {
|
|
65
|
-
"wdio-mcp": "lib/server.js"
|
|
69
|
+
"wdio-mcp": "lib/server.js",
|
|
70
|
+
"wdio-show-trace": "lib/show-trace.js"
|
|
66
71
|
},
|
|
67
72
|
license: "MIT",
|
|
68
73
|
publishConfig: {
|
|
@@ -95,11 +100,14 @@ var package_default = {
|
|
|
95
100
|
sharp: "^0.34.5",
|
|
96
101
|
webdriverio: "^9.27.0",
|
|
97
102
|
xpath: "^0.0.34",
|
|
103
|
+
yazl: "^3.3.1",
|
|
98
104
|
zod: "^4.3.6"
|
|
99
105
|
},
|
|
100
106
|
devDependencies: {
|
|
101
107
|
"@release-it/conventional-changelog": "^10.0.6",
|
|
102
108
|
"@types/node": "^20.19.37",
|
|
109
|
+
"@types/yauzl": "^2.10.3",
|
|
110
|
+
"@types/yazl": "^3.3.1",
|
|
103
111
|
"@wdio/eslint": "^0.1.3",
|
|
104
112
|
"@wdio/types": "^9.27.0",
|
|
105
113
|
eslint: "^9.39.4",
|
|
@@ -111,7 +119,8 @@ var package_default = {
|
|
|
111
119
|
tsup: "^8.5.1",
|
|
112
120
|
tsx: "^4.21.0",
|
|
113
121
|
typescript: "~5.9.3",
|
|
114
|
-
vitest: "^4.1.2"
|
|
122
|
+
vitest: "^4.1.2",
|
|
123
|
+
yauzl: "^3.3.0"
|
|
115
124
|
},
|
|
116
125
|
packageManager: "pnpm@10.32.1",
|
|
117
126
|
pnpm: {
|
|
@@ -119,7 +128,7 @@ var package_default = {
|
|
|
119
128
|
"release-it>undici": "^6.24.0",
|
|
120
129
|
"basic-ftp": "^5.3.1",
|
|
121
130
|
lodash: "^4.18.1",
|
|
122
|
-
hono: "^4.12.
|
|
131
|
+
hono: "^4.12.18",
|
|
123
132
|
"@hono/node-server": "^1.19.14",
|
|
124
133
|
"fast-xml-parser": "^5.7.3",
|
|
125
134
|
defu: "^6.1.7",
|
|
@@ -2116,6 +2125,230 @@ function withRecording(toolName, callback) {
|
|
|
2116
2125
|
};
|
|
2117
2126
|
}
|
|
2118
2127
|
|
|
2128
|
+
// src/trace/recorder.ts
|
|
2129
|
+
init_state();
|
|
2130
|
+
import sharp from "sharp";
|
|
2131
|
+
|
|
2132
|
+
// src/trace/state.ts
|
|
2133
|
+
import { createRequire } from "module";
|
|
2134
|
+
var require2 = createRequire(import.meta.url);
|
|
2135
|
+
var { version: LIBRARY_VERSION } = require2("../../package.json");
|
|
2136
|
+
var traceSessions = /* @__PURE__ */ new Map();
|
|
2137
|
+
function createTraceSession(sessionId, browserName, viewport, title, sessionType = "browser") {
|
|
2138
|
+
const prefix = sessionId.slice(0, 8);
|
|
2139
|
+
const session = {
|
|
2140
|
+
sessionId,
|
|
2141
|
+
startWallTime: Date.now(),
|
|
2142
|
+
startHrTime: process.hrtime.bigint(),
|
|
2143
|
+
pageId: `page@${prefix}`,
|
|
2144
|
+
contextId: `context@${prefix}`,
|
|
2145
|
+
callCounter: 0,
|
|
2146
|
+
events: [],
|
|
2147
|
+
screenshots: [],
|
|
2148
|
+
browserName,
|
|
2149
|
+
viewport,
|
|
2150
|
+
sessionType,
|
|
2151
|
+
lastAfterEndTime: 0,
|
|
2152
|
+
screenshotChain: Promise.resolve()
|
|
2153
|
+
};
|
|
2154
|
+
session.events.push({
|
|
2155
|
+
version: 8,
|
|
2156
|
+
type: "context-options",
|
|
2157
|
+
origin: "library",
|
|
2158
|
+
libraryName: "@wdio/mcp",
|
|
2159
|
+
libraryVersion: LIBRARY_VERSION,
|
|
2160
|
+
browserName,
|
|
2161
|
+
platform: process.platform === "darwin" ? "darwin" : process.platform === "win32" ? "windows" : "linux",
|
|
2162
|
+
wallTime: session.startWallTime,
|
|
2163
|
+
monotonicTime: 0,
|
|
2164
|
+
sdkLanguage: "javascript",
|
|
2165
|
+
title,
|
|
2166
|
+
contextId: session.contextId,
|
|
2167
|
+
options: { viewport }
|
|
2168
|
+
});
|
|
2169
|
+
traceSessions.set(sessionId, session);
|
|
2170
|
+
return session;
|
|
2171
|
+
}
|
|
2172
|
+
function getTraceSession(sessionId) {
|
|
2173
|
+
return traceSessions.get(sessionId);
|
|
2174
|
+
}
|
|
2175
|
+
function deleteTraceSession(sessionId) {
|
|
2176
|
+
traceSessions.delete(sessionId);
|
|
2177
|
+
}
|
|
2178
|
+
function getMonotonicMs(session) {
|
|
2179
|
+
return Number((process.hrtime.bigint() - session.startHrTime) / 1000000n);
|
|
2180
|
+
}
|
|
2181
|
+
|
|
2182
|
+
// src/trace/tool-mapping.ts
|
|
2183
|
+
var TOOL_MAP = {
|
|
2184
|
+
navigate: { class: "Page", method: "navigate" },
|
|
2185
|
+
click_element: { class: "Element", method: "click" },
|
|
2186
|
+
set_value: { class: "Element", method: "fill" },
|
|
2187
|
+
scroll: { class: "Page", method: "scroll" },
|
|
2188
|
+
tap_element: { class: "Element", method: "tap" },
|
|
2189
|
+
swipe: { class: "Page", method: "swipe" },
|
|
2190
|
+
drag_and_drop: { class: "Element", method: "dragTo" },
|
|
2191
|
+
execute_script: { class: "Page", method: "evaluate" },
|
|
2192
|
+
launch_chrome: { class: "Browser", method: "launch" }
|
|
2193
|
+
};
|
|
2194
|
+
function mapToolToTraceAction(toolName) {
|
|
2195
|
+
return TOOL_MAP[toolName] ?? null;
|
|
2196
|
+
}
|
|
2197
|
+
function extractSelectorLabel(selector) {
|
|
2198
|
+
const uiautomator = selector.match(/\.(?:text|description|textContains)\("([^"]+)"\)/);
|
|
2199
|
+
if (uiautomator) return uiautomator[1];
|
|
2200
|
+
if (selector.startsWith("~")) return selector.slice(1);
|
|
2201
|
+
const predicate = selector.match(/(?:label|name|value)\s*==\s*"([^"]+)"/);
|
|
2202
|
+
if (predicate) return predicate[1];
|
|
2203
|
+
const xpath2 = selector.match(/@(?:text|label|name|content-desc)="([^"]+)"/);
|
|
2204
|
+
if (xpath2) return xpath2[1];
|
|
2205
|
+
return selector;
|
|
2206
|
+
}
|
|
2207
|
+
function formatActionTitle(action, params) {
|
|
2208
|
+
const { class: cls, method } = action;
|
|
2209
|
+
const firstKey = Object.keys(params)[0];
|
|
2210
|
+
const firstVal = Object.values(params)[0];
|
|
2211
|
+
if (firstVal === void 0) return `${cls}.${method}()`;
|
|
2212
|
+
const raw = String(firstVal);
|
|
2213
|
+
const label = firstKey === "selector" ? extractSelectorLabel(raw) : raw;
|
|
2214
|
+
return `${cls}.${method}("${label.slice(0, 80)}")`;
|
|
2215
|
+
}
|
|
2216
|
+
|
|
2217
|
+
// src/trace/recorder.ts
|
|
2218
|
+
function startTrace(sessionId, capabilities, sessionType = "browser", browserViewport) {
|
|
2219
|
+
let browserName;
|
|
2220
|
+
let viewport;
|
|
2221
|
+
let title;
|
|
2222
|
+
if (sessionType === "browser") {
|
|
2223
|
+
browserName = String(capabilities.browserName ?? "chromium");
|
|
2224
|
+
viewport = browserViewport ?? { width: 1920, height: 1080 };
|
|
2225
|
+
title = String(capabilities.browserName ?? browserName);
|
|
2226
|
+
} else {
|
|
2227
|
+
browserName = "chromium";
|
|
2228
|
+
const deviceName = String(capabilities["appium:deviceName"] ?? capabilities.deviceName ?? "device");
|
|
2229
|
+
const platformVersion = capabilities["appium:platformVersion"] ?? capabilities.platformVersion ?? "";
|
|
2230
|
+
title = `${sessionType} - ${deviceName}${platformVersion ? ` (${platformVersion})` : ""}`;
|
|
2231
|
+
viewport = sessionType === "ios" ? { width: 390, height: 844 } : { width: 412, height: 915 };
|
|
2232
|
+
}
|
|
2233
|
+
createTraceSession(sessionId, browserName, viewport, title, sessionType);
|
|
2234
|
+
}
|
|
2235
|
+
function endTrace(_sessionId) {
|
|
2236
|
+
}
|
|
2237
|
+
async function recordInitialNavigation(sessionId, url) {
|
|
2238
|
+
const traceSession = getTraceSession(sessionId);
|
|
2239
|
+
if (!traceSession) return;
|
|
2240
|
+
const callId = `call@${++traceSession.callCounter}`;
|
|
2241
|
+
const startTime = getMonotonicMs(traceSession);
|
|
2242
|
+
traceSession.events.push({
|
|
2243
|
+
type: "before",
|
|
2244
|
+
callId,
|
|
2245
|
+
startTime,
|
|
2246
|
+
class: "Page",
|
|
2247
|
+
method: "navigate",
|
|
2248
|
+
pageId: traceSession.pageId,
|
|
2249
|
+
params: { url },
|
|
2250
|
+
title: `Page.navigate("${url.slice(0, 80)}")`
|
|
2251
|
+
});
|
|
2252
|
+
await captureScreenshot(traceSession);
|
|
2253
|
+
const navEndTime = getMonotonicMs(traceSession);
|
|
2254
|
+
traceSession.events.push({
|
|
2255
|
+
type: "after",
|
|
2256
|
+
callId,
|
|
2257
|
+
endTime: navEndTime
|
|
2258
|
+
});
|
|
2259
|
+
traceSession.lastAfterEndTime = navEndTime;
|
|
2260
|
+
}
|
|
2261
|
+
function withTrace(toolName, callback) {
|
|
2262
|
+
return async (params, extra) => {
|
|
2263
|
+
const state2 = getState();
|
|
2264
|
+
const sessionId = state2.currentSession;
|
|
2265
|
+
if (!sessionId) return callback(params, extra);
|
|
2266
|
+
const metadata = state2.sessionMetadata.get(sessionId);
|
|
2267
|
+
if (!metadata?.trace) return callback(params, extra);
|
|
2268
|
+
const traceSession = getTraceSession(sessionId);
|
|
2269
|
+
if (!traceSession) return callback(params, extra);
|
|
2270
|
+
const action = mapToolToTraceAction(toolName);
|
|
2271
|
+
if (!action) return callback(params, extra);
|
|
2272
|
+
await captureScreenshot(traceSession);
|
|
2273
|
+
if (traceSession.sessionType !== "browser") {
|
|
2274
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
2275
|
+
}
|
|
2276
|
+
const callId = `call@${++traceSession.callCounter}`;
|
|
2277
|
+
const startTime = getMonotonicMs(traceSession);
|
|
2278
|
+
traceSession.events.push({
|
|
2279
|
+
type: "before",
|
|
2280
|
+
callId,
|
|
2281
|
+
startTime,
|
|
2282
|
+
class: action.class,
|
|
2283
|
+
method: action.method,
|
|
2284
|
+
pageId: traceSession.pageId,
|
|
2285
|
+
params,
|
|
2286
|
+
title: formatActionTitle(action, params)
|
|
2287
|
+
});
|
|
2288
|
+
let result;
|
|
2289
|
+
let actionError;
|
|
2290
|
+
try {
|
|
2291
|
+
result = await callback(params, extra);
|
|
2292
|
+
if (result.isError) {
|
|
2293
|
+
const text = result.content?.find((c) => c.type === "text")?.text;
|
|
2294
|
+
actionError = text ? String(text) : "unknown error";
|
|
2295
|
+
}
|
|
2296
|
+
} catch (e) {
|
|
2297
|
+
actionError = String(e);
|
|
2298
|
+
const errorEndTime = getMonotonicMs(traceSession);
|
|
2299
|
+
traceSession.events.push({
|
|
2300
|
+
type: "after",
|
|
2301
|
+
callId,
|
|
2302
|
+
endTime: errorEndTime,
|
|
2303
|
+
error: { message: actionError }
|
|
2304
|
+
});
|
|
2305
|
+
traceSession.lastAfterEndTime = errorEndTime;
|
|
2306
|
+
throw e;
|
|
2307
|
+
}
|
|
2308
|
+
const endTime = getMonotonicMs(traceSession);
|
|
2309
|
+
traceSession.events.push({
|
|
2310
|
+
type: "after",
|
|
2311
|
+
callId,
|
|
2312
|
+
endTime,
|
|
2313
|
+
...actionError ? { error: { message: actionError } } : {}
|
|
2314
|
+
});
|
|
2315
|
+
traceSession.lastAfterEndTime = endTime;
|
|
2316
|
+
return result;
|
|
2317
|
+
};
|
|
2318
|
+
}
|
|
2319
|
+
function captureTraceScreenshot(sessionId, browser) {
|
|
2320
|
+
const traceSession = getTraceSession(sessionId);
|
|
2321
|
+
if (!traceSession) return;
|
|
2322
|
+
const p = captureScreenshot(traceSession, browser);
|
|
2323
|
+
traceSession.screenshotChain = traceSession.screenshotChain.then(() => p);
|
|
2324
|
+
}
|
|
2325
|
+
async function captureScreenshot(traceSession, browser) {
|
|
2326
|
+
try {
|
|
2327
|
+
const b = browser ?? getBrowser();
|
|
2328
|
+
const base64 = await b.takeScreenshot();
|
|
2329
|
+
const inputBuffer = Buffer.from(base64, "base64");
|
|
2330
|
+
const image = sharp(inputBuffer);
|
|
2331
|
+
const metadata = await image.metadata();
|
|
2332
|
+
const width = metadata.width ?? 1280;
|
|
2333
|
+
const height = metadata.height ?? 720;
|
|
2334
|
+
const jpegBuffer = await image.jpeg({ quality: 60 }).toBuffer();
|
|
2335
|
+
const wallTimestamp = traceSession.startWallTime + getMonotonicMs(traceSession);
|
|
2336
|
+
const resourceName = `${traceSession.pageId}-${wallTimestamp}.jpeg`;
|
|
2337
|
+
traceSession.screenshots.push({ resourceName, data: jpegBuffer, width, height });
|
|
2338
|
+
traceSession.events.push({
|
|
2339
|
+
type: "screencast-frame",
|
|
2340
|
+
pageId: traceSession.pageId,
|
|
2341
|
+
sha1: resourceName,
|
|
2342
|
+
width,
|
|
2343
|
+
height,
|
|
2344
|
+
// Stamp at the previous action's endTime so the player shows this frame
|
|
2345
|
+
// as the result of that action, not as the "before" state of the next one.
|
|
2346
|
+
timestamp: traceSession.lastAfterEndTime
|
|
2347
|
+
});
|
|
2348
|
+
} catch {
|
|
2349
|
+
}
|
|
2350
|
+
}
|
|
2351
|
+
|
|
2119
2352
|
// src/resources/browserstack-local.resource.ts
|
|
2120
2353
|
function getLocalBinaryInfo() {
|
|
2121
2354
|
const platform2 = process.platform;
|
|
@@ -2833,12 +3066,12 @@ var accessibilityResource = {
|
|
|
2833
3066
|
|
|
2834
3067
|
// src/resources/screenshot.resource.ts
|
|
2835
3068
|
init_state();
|
|
2836
|
-
import
|
|
3069
|
+
import sharp2 from "sharp";
|
|
2837
3070
|
var MAX_DIMENSION = 2e3;
|
|
2838
3071
|
var MAX_FILE_SIZE_BYTES = 1024 * 1024;
|
|
2839
3072
|
async function processScreenshot(screenshotBase64) {
|
|
2840
3073
|
const inputBuffer = Buffer.from(screenshotBase64, "base64");
|
|
2841
|
-
let image =
|
|
3074
|
+
let image = sharp2(inputBuffer);
|
|
2842
3075
|
const metadata = await image.metadata();
|
|
2843
3076
|
const width = metadata.width ?? 0;
|
|
2844
3077
|
const height = metadata.height ?? 0;
|
|
@@ -3044,6 +3277,8 @@ import { z as z14 } from "zod";
|
|
|
3044
3277
|
|
|
3045
3278
|
// src/session/lifecycle.ts
|
|
3046
3279
|
init_state();
|
|
3280
|
+
import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
3281
|
+
import { join as join3 } from "path";
|
|
3047
3282
|
|
|
3048
3283
|
// src/providers/local-browser.provider.ts
|
|
3049
3284
|
var LocalBrowserProvider = class {
|
|
@@ -3410,7 +3645,47 @@ function getProvider(providerName, platform2) {
|
|
|
3410
3645
|
return platform2 === "browser" ? localBrowserProvider : localAppiumProvider;
|
|
3411
3646
|
}
|
|
3412
3647
|
|
|
3648
|
+
// src/trace/zip-writer.ts
|
|
3649
|
+
import yazl from "yazl";
|
|
3650
|
+
function buildTraceZip(session) {
|
|
3651
|
+
return new Promise((resolve, reject) => {
|
|
3652
|
+
const zipFile = new yazl.ZipFile();
|
|
3653
|
+
const traceNdjson = session.events.map((e) => JSON.stringify(e)).join("\n");
|
|
3654
|
+
const traceBuffer = Buffer.from(traceNdjson, "utf8");
|
|
3655
|
+
zipFile.addBuffer(traceBuffer, "trace.trace");
|
|
3656
|
+
zipFile.addBuffer(Buffer.alloc(0), "trace.network");
|
|
3657
|
+
for (const screenshot of session.screenshots) {
|
|
3658
|
+
zipFile.addBuffer(screenshot.data, `resources/${screenshot.resourceName}`);
|
|
3659
|
+
}
|
|
3660
|
+
zipFile.end();
|
|
3661
|
+
const chunks = [];
|
|
3662
|
+
zipFile.outputStream.on("data", (chunk) => chunks.push(chunk));
|
|
3663
|
+
zipFile.outputStream.on("end", () => resolve(Buffer.concat(chunks)));
|
|
3664
|
+
zipFile.outputStream.on("error", reject);
|
|
3665
|
+
});
|
|
3666
|
+
}
|
|
3667
|
+
|
|
3413
3668
|
// src/session/lifecycle.ts
|
|
3669
|
+
async function finalizeTrace(sessionId, browser) {
|
|
3670
|
+
endTrace(sessionId);
|
|
3671
|
+
captureTraceScreenshot(sessionId, browser);
|
|
3672
|
+
const traceSession = getTraceSession(sessionId);
|
|
3673
|
+
if (!traceSession) return;
|
|
3674
|
+
try {
|
|
3675
|
+
await traceSession.screenshotChain;
|
|
3676
|
+
const traceDir = join3(process.cwd(), ".trace");
|
|
3677
|
+
mkdirSync2(traceDir, { recursive: true });
|
|
3678
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
3679
|
+
const outPath = join3(traceDir, `${timestamp}-${sessionId.slice(0, 8)}.zip`);
|
|
3680
|
+
const zipBuffer = await buildTraceZip(traceSession);
|
|
3681
|
+
writeFileSync2(outPath, zipBuffer);
|
|
3682
|
+
console.error(`[TRACE] Saved to ${outPath}`);
|
|
3683
|
+
} catch (e) {
|
|
3684
|
+
console.error("[TRACE] Failed to save trace:", e);
|
|
3685
|
+
} finally {
|
|
3686
|
+
deleteTraceSession(sessionId);
|
|
3687
|
+
}
|
|
3688
|
+
}
|
|
3414
3689
|
function getSessionResult(history) {
|
|
3415
3690
|
const errorStep = history?.steps.find((s) => s.status === "error");
|
|
3416
3691
|
return errorStep ? { status: "failed", reason: errorStep.error } : { status: "passed" };
|
|
@@ -3447,6 +3722,9 @@ function registerSession(sessionId, browser, metadata, historyEntry) {
|
|
|
3447
3722
|
const oldMetadata = state2.sessionMetadata.get(oldSessionId);
|
|
3448
3723
|
if (oldBrowser) {
|
|
3449
3724
|
void (async () => {
|
|
3725
|
+
if (oldMetadata?.trace) {
|
|
3726
|
+
await finalizeTrace(oldSessionId, oldBrowser);
|
|
3727
|
+
}
|
|
3450
3728
|
if (oldMetadata?.provider) {
|
|
3451
3729
|
const oldHistory = state2.sessionHistory.get(oldSessionId);
|
|
3452
3730
|
const provider = getProvider(oldMetadata.provider, oldMetadata.type);
|
|
@@ -3470,6 +3748,9 @@ async function closeSession(sessionId, detach, isAttached, force) {
|
|
|
3470
3748
|
history.endedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
3471
3749
|
}
|
|
3472
3750
|
const metadata = state2.sessionMetadata.get(sessionId);
|
|
3751
|
+
if (metadata?.trace) {
|
|
3752
|
+
await finalizeTrace(sessionId, browser);
|
|
3753
|
+
}
|
|
3473
3754
|
if (force || !detach && !isAttached) {
|
|
3474
3755
|
if (metadata?.provider) {
|
|
3475
3756
|
try {
|
|
@@ -3524,6 +3805,7 @@ var startSessionToolDefinition = {
|
|
|
3524
3805
|
noReset: coerceBoolean.optional().describe("Preserve app data between sessions"),
|
|
3525
3806
|
fullReset: coerceBoolean.optional().describe("Uninstall app before/after session"),
|
|
3526
3807
|
newCommandTimeout: z14.number().min(0).optional().default(300).describe("Appium command timeout in seconds"),
|
|
3808
|
+
trace: coerceBoolean.optional().default(false).describe("Enable trace recording \u2014 produces a Playwright-compatible zip saved to .trace/ on close_session, playable at player.vibium.dev."),
|
|
3527
3809
|
attach: coerceBoolean.optional().default(false).describe("Attach to existing Chrome instead of launching"),
|
|
3528
3810
|
attachConfig: z14.object({
|
|
3529
3811
|
port: z14.number().optional().default(9222),
|
|
@@ -3630,7 +3912,8 @@ async function startBrowserSession(args) {
|
|
|
3630
3912
|
capabilities: mergedCapabilities,
|
|
3631
3913
|
isAttached: false,
|
|
3632
3914
|
provider: args.provider ?? "local",
|
|
3633
|
-
tunnelHandle
|
|
3915
|
+
tunnelHandle,
|
|
3916
|
+
trace: args.trace ?? false
|
|
3634
3917
|
};
|
|
3635
3918
|
registerSession(sessionId, wdioBrowser, sessionMetadata, {
|
|
3636
3919
|
sessionId,
|
|
@@ -3639,6 +3922,9 @@ async function startBrowserSession(args) {
|
|
|
3639
3922
|
capabilities: mergedCapabilities,
|
|
3640
3923
|
steps: []
|
|
3641
3924
|
});
|
|
3925
|
+
if (args.trace) {
|
|
3926
|
+
startTrace(sessionId, mergedCapabilities, "browser", { width: windowWidth, height: windowHeight });
|
|
3927
|
+
}
|
|
3642
3928
|
let sizeNote = "";
|
|
3643
3929
|
try {
|
|
3644
3930
|
await wdioBrowser.setWindowSize(windowWidth, windowHeight);
|
|
@@ -3648,6 +3934,9 @@ Note: Unable to set window size (${windowWidth}x${windowHeight}). ${e}`;
|
|
|
3648
3934
|
}
|
|
3649
3935
|
if (navigationUrl) {
|
|
3650
3936
|
await wdioBrowser.url(navigationUrl);
|
|
3937
|
+
if (args.trace) {
|
|
3938
|
+
await recordInitialNavigation(sessionId, navigationUrl);
|
|
3939
|
+
}
|
|
3651
3940
|
}
|
|
3652
3941
|
const modeText = effectiveHeadless ? "headless" : "headed";
|
|
3653
3942
|
const urlText = navigationUrl ? ` and navigated to ${navigationUrl}` : "";
|
|
@@ -3682,7 +3971,8 @@ async function startMobileSession(args) {
|
|
|
3682
3971
|
capabilities: mergedCapabilities,
|
|
3683
3972
|
isAttached: shouldAutoDetach,
|
|
3684
3973
|
provider: args.provider ?? "local",
|
|
3685
|
-
tunnelHandle
|
|
3974
|
+
tunnelHandle,
|
|
3975
|
+
trace: args.trace ?? false
|
|
3686
3976
|
};
|
|
3687
3977
|
registerSession(sessionId, browser, metadata, {
|
|
3688
3978
|
sessionId,
|
|
@@ -3692,6 +3982,9 @@ async function startMobileSession(args) {
|
|
|
3692
3982
|
appiumConfig: { hostname: serverConfig.hostname, port: serverConfig.port, path: serverConfig.path },
|
|
3693
3983
|
steps: []
|
|
3694
3984
|
});
|
|
3985
|
+
if (args.trace) {
|
|
3986
|
+
startTrace(sessionId, mergedCapabilities, sessionType);
|
|
3987
|
+
}
|
|
3695
3988
|
const appInfo = appPath ? `
|
|
3696
3989
|
App: ${appPath}` : "\nApp: (connected to running app)";
|
|
3697
3990
|
const detachNote = shouldAutoDetach ? "\n\n(Auto-detach enabled: session will be preserved on close. Use close_session({ detach: false }) to force terminate.)" : "";
|
|
@@ -3729,7 +4022,8 @@ async function attachBrowserSession(args) {
|
|
|
3729
4022
|
type: "browser",
|
|
3730
4023
|
capabilities,
|
|
3731
4024
|
isAttached: true,
|
|
3732
|
-
provider: "local"
|
|
4025
|
+
provider: "local",
|
|
4026
|
+
trace: args.trace ?? false
|
|
3733
4027
|
};
|
|
3734
4028
|
registerSession(sessionId, browser, sessionMetadata, {
|
|
3735
4029
|
sessionId,
|
|
@@ -3738,10 +4032,19 @@ async function attachBrowserSession(args) {
|
|
|
3738
4032
|
capabilities,
|
|
3739
4033
|
steps: []
|
|
3740
4034
|
});
|
|
4035
|
+
if (args.trace) {
|
|
4036
|
+
startTrace(sessionId, capabilities, "browser", { width: 1920, height: 1080 });
|
|
4037
|
+
}
|
|
3741
4038
|
if (navigationUrl) {
|
|
3742
4039
|
await browser.url(navigationUrl);
|
|
4040
|
+
if (args.trace) {
|
|
4041
|
+
await recordInitialNavigation(sessionId, navigationUrl);
|
|
4042
|
+
}
|
|
3743
4043
|
} else if (activeTabUrl) {
|
|
3744
4044
|
await restoreAndSwitchToActiveTab(browser, activeTabUrl, allTabUrls);
|
|
4045
|
+
if (args.trace) {
|
|
4046
|
+
await recordInitialNavigation(sessionId, activeTabUrl);
|
|
4047
|
+
}
|
|
3745
4048
|
}
|
|
3746
4049
|
const title = await browser.getTitle();
|
|
3747
4050
|
const url = await browser.getUrl();
|
|
@@ -4094,26 +4397,27 @@ function createServer() {
|
|
|
4094
4397
|
);
|
|
4095
4398
|
}
|
|
4096
4399
|
};
|
|
4400
|
+
const instrument = (name, cb) => withTrace(name, withRecording(name, cb));
|
|
4097
4401
|
registerTool(startSessionToolDefinition, withRecording("start_session", startSessionTool));
|
|
4098
4402
|
registerTool(closeSessionToolDefinition, closeSessionTool);
|
|
4099
|
-
registerTool(launchChromeToolDefinition,
|
|
4403
|
+
registerTool(launchChromeToolDefinition, instrument("launch_chrome", launchChromeTool));
|
|
4100
4404
|
registerTool(emulateDeviceToolDefinition, emulateDeviceTool);
|
|
4101
|
-
registerTool(navigateToolDefinition,
|
|
4405
|
+
registerTool(navigateToolDefinition, instrument("navigate", navigateTool));
|
|
4102
4406
|
registerTool(switchTabToolDefinition, switchTabTool);
|
|
4103
4407
|
registerTool(switchFrameToolDefinition, switchFrameTool);
|
|
4104
|
-
registerTool(scrollToolDefinition,
|
|
4105
|
-
registerTool(clickToolDefinition,
|
|
4106
|
-
registerTool(setValueToolDefinition,
|
|
4408
|
+
registerTool(scrollToolDefinition, instrument("scroll", scrollTool));
|
|
4409
|
+
registerTool(clickToolDefinition, instrument("click_element", clickTool));
|
|
4410
|
+
registerTool(setValueToolDefinition, instrument("set_value", setValueTool));
|
|
4107
4411
|
registerTool(setCookieToolDefinition, setCookieTool);
|
|
4108
4412
|
registerTool(deleteCookiesToolDefinition, deleteCookiesTool);
|
|
4109
|
-
registerTool(tapElementToolDefinition,
|
|
4110
|
-
registerTool(swipeToolDefinition,
|
|
4111
|
-
registerTool(dragAndDropToolDefinition,
|
|
4413
|
+
registerTool(tapElementToolDefinition, instrument("tap_element", tapElementTool));
|
|
4414
|
+
registerTool(swipeToolDefinition, instrument("swipe", swipeTool));
|
|
4415
|
+
registerTool(dragAndDropToolDefinition, instrument("drag_and_drop", dragAndDropTool));
|
|
4112
4416
|
registerTool(switchContextToolDefinition, switchContextTool);
|
|
4113
4417
|
registerTool(rotateDeviceToolDefinition, rotateDeviceTool);
|
|
4114
4418
|
registerTool(hideKeyboardToolDefinition, hideKeyboardTool);
|
|
4115
4419
|
registerTool(setGeolocationToolDefinition, setGeolocationTool);
|
|
4116
|
-
registerTool(executeScriptToolDefinition,
|
|
4420
|
+
registerTool(executeScriptToolDefinition, instrument("execute_script", executeScriptTool));
|
|
4117
4421
|
registerTool(getElementsToolDefinition, getElementsTool);
|
|
4118
4422
|
registerTool(listAppsToolDefinition, listAppsTool);
|
|
4119
4423
|
registerTool(uploadAppToolDefinition, uploadAppTool);
|