@rolepod/uiproof 0.4.0 → 0.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/.claude-plugin/marketplace.json +3 -3
- package/.claude-plugin/plugin.json +3 -3
- package/.codex-plugin/plugin.json +4 -10
- package/.cursor/mcp.json +1 -1
- package/.cursor-plugin/plugin.json +2 -2
- package/CHANGELOG.md +121 -0
- package/README.md +12 -8
- package/dist/bin/rolepod-uiproof.js +1590 -33
- package/dist/bin/rolepod-uiproof.js.map +1 -1
- package/dist/index.d.ts +590 -2
- package/dist/index.js +1612 -33
- package/dist/index.js.map +1 -1
- package/dist/schemas/tools.json +34 -1
- package/package.json +1 -1
- package/skills/check-errors/SKILL.md +114 -0
- package/skills/scaffold-e2e/SKILL.md +14 -0
- package/skills/verify-ui/SKILL.md +138 -71
|
@@ -582,6 +582,34 @@ var AppiumEngine = class {
|
|
|
582
582
|
throw new UnsupportedPlatformError(_session.platform);
|
|
583
583
|
}
|
|
584
584
|
// -------------------------------------------------------------------------
|
|
585
|
+
// v0.5 cross-platform additions — mobile stubs.
|
|
586
|
+
// These ship as `not_implemented_in_v05` until the mobile gesture work lands.
|
|
587
|
+
// -------------------------------------------------------------------------
|
|
588
|
+
async hover(_session, _ref) {
|
|
589
|
+
throw new RolepodMcpError(
|
|
590
|
+
"engine_error",
|
|
591
|
+
"hover is not yet implemented for mobile (Appium). Use long-press via custom gesture if needed."
|
|
592
|
+
);
|
|
593
|
+
}
|
|
594
|
+
async drag(_session, _fromRef, _toRef) {
|
|
595
|
+
throw new RolepodMcpError(
|
|
596
|
+
"engine_error",
|
|
597
|
+
"drag is not yet implemented for mobile (Appium). Use the W3C Actions API directly if needed."
|
|
598
|
+
);
|
|
599
|
+
}
|
|
600
|
+
async fillForm(session, fields) {
|
|
601
|
+
for (const f of fields) {
|
|
602
|
+
const v = typeof f.value === "boolean" ? String(f.value) : f.value;
|
|
603
|
+
await this.type(session, f.ref, v);
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
async uploadFile(_session, _ref, _filePath) {
|
|
607
|
+
throw new RolepodMcpError(
|
|
608
|
+
"engine_error",
|
|
609
|
+
"upload_file is not supported on mobile (Appium)."
|
|
610
|
+
);
|
|
611
|
+
}
|
|
612
|
+
// -------------------------------------------------------------------------
|
|
585
613
|
// Internals
|
|
586
614
|
// -------------------------------------------------------------------------
|
|
587
615
|
async loadWdio() {
|
|
@@ -696,6 +724,7 @@ function treeIncludesText(node, text) {
|
|
|
696
724
|
|
|
697
725
|
// src/engine/PlaywrightEngine.ts
|
|
698
726
|
import { randomUUID as randomUUID3 } from "crypto";
|
|
727
|
+
import { resolve as resolvePath, isAbsolute } from "path";
|
|
699
728
|
import {
|
|
700
729
|
chromium,
|
|
701
730
|
firefox,
|
|
@@ -805,6 +834,83 @@ function parseAriaSnapshot(snapshotYaml) {
|
|
|
805
834
|
}
|
|
806
835
|
|
|
807
836
|
// src/engine/PlaywrightEngine.ts
|
|
837
|
+
var CONSOLE_BUFFER_CAP = 1e3;
|
|
838
|
+
var NETWORK_BUFFER_CAP = 1e3;
|
|
839
|
+
var NETWORK_PRESETS = {
|
|
840
|
+
offline: { offline: true, downloadThroughput: 0, uploadThroughput: 0, latency: 0 },
|
|
841
|
+
"slow-3g": {
|
|
842
|
+
offline: false,
|
|
843
|
+
// 500 Kbps down / 500 Kbps up / 400ms RTT
|
|
844
|
+
downloadThroughput: 500 * 1024 / 8,
|
|
845
|
+
uploadThroughput: 500 * 1024 / 8,
|
|
846
|
+
latency: 400
|
|
847
|
+
},
|
|
848
|
+
"fast-3g": {
|
|
849
|
+
offline: false,
|
|
850
|
+
downloadThroughput: 1.5 * 1024 * 1024 / 8,
|
|
851
|
+
uploadThroughput: 750 * 1024 / 8,
|
|
852
|
+
latency: 150
|
|
853
|
+
},
|
|
854
|
+
"slow-4g": {
|
|
855
|
+
offline: false,
|
|
856
|
+
downloadThroughput: 4 * 1024 * 1024 / 8,
|
|
857
|
+
uploadThroughput: 3 * 1024 * 1024 / 8,
|
|
858
|
+
latency: 100
|
|
859
|
+
},
|
|
860
|
+
"fast-4g": {
|
|
861
|
+
offline: false,
|
|
862
|
+
downloadThroughput: 9 * 1024 * 1024 / 8,
|
|
863
|
+
uploadThroughput: 4.5 * 1024 * 1024 / 8,
|
|
864
|
+
latency: 60
|
|
865
|
+
},
|
|
866
|
+
"no-throttling": {
|
|
867
|
+
offline: false,
|
|
868
|
+
downloadThroughput: -1,
|
|
869
|
+
uploadThroughput: -1,
|
|
870
|
+
latency: 0
|
|
871
|
+
}
|
|
872
|
+
};
|
|
873
|
+
function pushRing(buf, entry, cap) {
|
|
874
|
+
buf.push(entry);
|
|
875
|
+
if (buf.length > cap) buf.splice(0, buf.length - cap);
|
|
876
|
+
}
|
|
877
|
+
function mapConsoleLevel(t) {
|
|
878
|
+
switch (t) {
|
|
879
|
+
case "error":
|
|
880
|
+
return "error";
|
|
881
|
+
case "warning":
|
|
882
|
+
return "warning";
|
|
883
|
+
case "info":
|
|
884
|
+
return "info";
|
|
885
|
+
case "debug":
|
|
886
|
+
return "debug";
|
|
887
|
+
case "trace":
|
|
888
|
+
return "trace";
|
|
889
|
+
default:
|
|
890
|
+
return "log";
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
function formatConsoleLocation(msg) {
|
|
894
|
+
try {
|
|
895
|
+
const loc = msg.location();
|
|
896
|
+
if (!loc?.url) return void 0;
|
|
897
|
+
return `${loc.url}:${loc.lineNumber}:${loc.columnNumber}`;
|
|
898
|
+
} catch {
|
|
899
|
+
return void 0;
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
function findNetworkEntry(buf, req) {
|
|
903
|
+
const url = req.url();
|
|
904
|
+
const method = req.method();
|
|
905
|
+
for (let i = buf.length - 1; i >= 0; i--) {
|
|
906
|
+
const e = buf[i];
|
|
907
|
+
if (!e) continue;
|
|
908
|
+
if (e.url === url && e.method === method && e.status === void 0 && !e.failure) {
|
|
909
|
+
return e;
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
return void 0;
|
|
913
|
+
}
|
|
808
914
|
var PlaywrightEngine = class {
|
|
809
915
|
id = "playwright";
|
|
810
916
|
sessions = /* @__PURE__ */ new Map();
|
|
@@ -820,35 +926,80 @@ var PlaywrightEngine = class {
|
|
|
820
926
|
if (opts.viewport) contextOptions.viewport = opts.viewport;
|
|
821
927
|
if (opts.user_agent) contextOptions.userAgent = opts.user_agent;
|
|
822
928
|
if (opts.locale) contextOptions.locale = opts.locale;
|
|
929
|
+
if (opts.capture?.har) {
|
|
930
|
+
contextOptions.recordHar = { path: opts.capture.har.path };
|
|
931
|
+
}
|
|
932
|
+
if (opts.capture?.video) {
|
|
933
|
+
contextOptions.recordVideo = {
|
|
934
|
+
dir: opts.capture.video.dir,
|
|
935
|
+
size: opts.capture.video.sizeWidth && opts.capture.video.sizeHeight ? {
|
|
936
|
+
width: opts.capture.video.sizeWidth,
|
|
937
|
+
height: opts.capture.video.sizeHeight
|
|
938
|
+
} : void 0
|
|
939
|
+
};
|
|
940
|
+
}
|
|
823
941
|
const context = await browser.newContext(contextOptions);
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
942
|
+
if (opts.capture?.trace) {
|
|
943
|
+
await context.tracing.start({
|
|
944
|
+
screenshots: true,
|
|
945
|
+
snapshots: true,
|
|
946
|
+
sources: false
|
|
947
|
+
});
|
|
827
948
|
}
|
|
949
|
+
const page = await context.newPage();
|
|
828
950
|
const sessionId = randomUUID3();
|
|
829
|
-
const
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
session,
|
|
951
|
+
const internals = {
|
|
952
|
+
session: {
|
|
953
|
+
id: sessionId,
|
|
954
|
+
platform: "web",
|
|
955
|
+
browser,
|
|
956
|
+
context,
|
|
957
|
+
mainPage: page
|
|
958
|
+
},
|
|
838
959
|
refIndex: /* @__PURE__ */ new Map(),
|
|
839
960
|
snapshotGeneration: 0,
|
|
840
961
|
refGeneration: -1,
|
|
841
|
-
lastSnapshotAt: null
|
|
962
|
+
lastSnapshotAt: null,
|
|
963
|
+
pages: [page],
|
|
964
|
+
activePageIndex: 0,
|
|
965
|
+
consoleBuffer: [],
|
|
966
|
+
networkBuffer: [],
|
|
967
|
+
networkInflight: /* @__PURE__ */ new Map(),
|
|
968
|
+
networkNextId: 1,
|
|
969
|
+
dialogArming: null,
|
|
970
|
+
captureOpts: opts.capture,
|
|
971
|
+
traceStarted: !!opts.capture?.trace
|
|
972
|
+
};
|
|
973
|
+
this.attachPageListeners(internals, page);
|
|
974
|
+
context.on("page", (newPage) => {
|
|
975
|
+
internals.pages.push(newPage);
|
|
976
|
+
this.attachPageListeners(internals, newPage);
|
|
842
977
|
});
|
|
978
|
+
if (opts.url) {
|
|
979
|
+
await page.goto(opts.url, { waitUntil: "domcontentloaded" });
|
|
980
|
+
}
|
|
981
|
+
this.sessions.set(sessionId, internals);
|
|
843
982
|
log.info("session opened", {
|
|
844
983
|
session_id: sessionId,
|
|
845
984
|
browser: browserName,
|
|
846
|
-
url: opts.url ?? null
|
|
985
|
+
url: opts.url ?? null,
|
|
986
|
+
capture: opts.capture ? Object.keys(opts.capture).filter(
|
|
987
|
+
(k) => opts.capture[k]
|
|
988
|
+
) : []
|
|
847
989
|
});
|
|
848
990
|
return { id: sessionId, platform: "web" };
|
|
849
991
|
}
|
|
850
992
|
async close(session) {
|
|
851
993
|
const s = this.requireSession(session.id);
|
|
994
|
+
if (s.traceStarted && s.captureOpts?.trace) {
|
|
995
|
+
const tracePath = resolvePath(s.captureOpts.trace.artifactDir, "trace.zip");
|
|
996
|
+
await s.session.context.tracing.stop({ path: tracePath }).catch((err) => {
|
|
997
|
+
log.warn("trace stop failed", {
|
|
998
|
+
session_id: session.id,
|
|
999
|
+
err: String(err)
|
|
1000
|
+
});
|
|
1001
|
+
});
|
|
1002
|
+
}
|
|
852
1003
|
await s.session.context.close().catch((err) => {
|
|
853
1004
|
log.warn("context close failed", { session_id: session.id, err: String(err) });
|
|
854
1005
|
});
|
|
@@ -860,7 +1011,7 @@ var PlaywrightEngine = class {
|
|
|
860
1011
|
}
|
|
861
1012
|
async snapshot(session, mode = "visible") {
|
|
862
1013
|
const s = this.requireSession(session.id);
|
|
863
|
-
const ariaYaml = await s.
|
|
1014
|
+
const ariaYaml = await this.activePage(s).ariaSnapshot({ mode: "ai" });
|
|
864
1015
|
const { tree, refIndex } = parseAriaSnapshot(ariaYaml);
|
|
865
1016
|
void mode;
|
|
866
1017
|
s.snapshotGeneration += 1;
|
|
@@ -870,7 +1021,7 @@ var PlaywrightEngine = class {
|
|
|
870
1021
|
return {
|
|
871
1022
|
session_id: session.id,
|
|
872
1023
|
platform: "web",
|
|
873
|
-
url_or_screen: s.
|
|
1024
|
+
url_or_screen: this.activePage(s).url(),
|
|
874
1025
|
taken_at: s.lastSnapshotAt,
|
|
875
1026
|
tree
|
|
876
1027
|
};
|
|
@@ -890,7 +1041,7 @@ var PlaywrightEngine = class {
|
|
|
890
1041
|
}
|
|
891
1042
|
async key(session, key) {
|
|
892
1043
|
const s = this.requireSession(session.id);
|
|
893
|
-
await s.
|
|
1044
|
+
await this.activePage(s).keyboard.press(key);
|
|
894
1045
|
this.invalidateRefs(s);
|
|
895
1046
|
}
|
|
896
1047
|
async scroll(session, dir, amount = 400, ref) {
|
|
@@ -904,13 +1055,13 @@ var PlaywrightEngine = class {
|
|
|
904
1055
|
[dx, dy]
|
|
905
1056
|
);
|
|
906
1057
|
} else {
|
|
907
|
-
await s.
|
|
1058
|
+
await this.activePage(s).mouse.wheel(dx, dy);
|
|
908
1059
|
}
|
|
909
1060
|
this.invalidateRefs(s);
|
|
910
1061
|
}
|
|
911
1062
|
async waitFor(session, cond, timeoutMs = 1e4) {
|
|
912
1063
|
const s = this.requireSession(session.id);
|
|
913
|
-
const page = s
|
|
1064
|
+
const page = this.activePage(s);
|
|
914
1065
|
switch (cond.kind) {
|
|
915
1066
|
case "text_visible":
|
|
916
1067
|
await page.getByText(cond.text, { exact: false }).first().waitFor({ state: "visible", timeout: timeoutMs });
|
|
@@ -930,14 +1081,14 @@ var PlaywrightEngine = class {
|
|
|
930
1081
|
}
|
|
931
1082
|
async screenshot(session, fullPage = false) {
|
|
932
1083
|
const s = this.requireSession(session.id);
|
|
933
|
-
return s.
|
|
1084
|
+
return this.activePage(s).screenshot({ fullPage });
|
|
934
1085
|
}
|
|
935
1086
|
async navigate(session, url) {
|
|
936
1087
|
const s = this.requireSession(session.id);
|
|
937
1088
|
if (s.session.platform !== "web") {
|
|
938
1089
|
throw new UnsupportedPlatformError(s.session.platform);
|
|
939
1090
|
}
|
|
940
|
-
await s.
|
|
1091
|
+
await this.activePage(s).goto(url, { waitUntil: "domcontentloaded" });
|
|
941
1092
|
this.invalidateRefs(s);
|
|
942
1093
|
}
|
|
943
1094
|
/**
|
|
@@ -951,7 +1102,7 @@ var PlaywrightEngine = class {
|
|
|
951
1102
|
if (s.session.platform !== "web") {
|
|
952
1103
|
throw new UnsupportedPlatformError(s.session.platform);
|
|
953
1104
|
}
|
|
954
|
-
return s
|
|
1105
|
+
return this.activePage(s);
|
|
955
1106
|
}
|
|
956
1107
|
/** Increment generation; the next ref-using call will see them as stale. */
|
|
957
1108
|
bumpGeneration(sessionId) {
|
|
@@ -959,8 +1110,311 @@ var PlaywrightEngine = class {
|
|
|
959
1110
|
this.invalidateRefs(s);
|
|
960
1111
|
}
|
|
961
1112
|
// -------------------------------------------------------------------------
|
|
1113
|
+
// v0.5 — input additions
|
|
1114
|
+
// -------------------------------------------------------------------------
|
|
1115
|
+
async hover(session, ref) {
|
|
1116
|
+
const s = this.requireSession(session.id);
|
|
1117
|
+
const locator = this.resolveLocator(s, ref);
|
|
1118
|
+
await locator.hover();
|
|
1119
|
+
}
|
|
1120
|
+
async drag(session, fromRef, toRef) {
|
|
1121
|
+
const s = this.requireSession(session.id);
|
|
1122
|
+
const from = this.resolveLocator(s, fromRef);
|
|
1123
|
+
const to = this.resolveLocator(s, toRef);
|
|
1124
|
+
await from.dragTo(to);
|
|
1125
|
+
this.invalidateRefs(s);
|
|
1126
|
+
}
|
|
1127
|
+
async fillForm(session, fields) {
|
|
1128
|
+
const s = this.requireSession(session.id);
|
|
1129
|
+
for (const field of fields) {
|
|
1130
|
+
const locator = this.resolveLocator(s, field.ref);
|
|
1131
|
+
const kind = field.kind;
|
|
1132
|
+
if (kind === "checkbox" || kind === "radio") {
|
|
1133
|
+
const checked = typeof field.value === "boolean" ? field.value : field.value === "true" || field.value === "on";
|
|
1134
|
+
await locator.setChecked(checked);
|
|
1135
|
+
} else if (kind === "select") {
|
|
1136
|
+
await locator.selectOption(String(field.value));
|
|
1137
|
+
} else {
|
|
1138
|
+
await locator.fill(String(field.value));
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
this.invalidateRefs(s);
|
|
1142
|
+
}
|
|
1143
|
+
async uploadFile(session, ref, filePath) {
|
|
1144
|
+
const s = this.requireSession(session.id);
|
|
1145
|
+
if (!isAbsolute(filePath)) {
|
|
1146
|
+
throw new RolepodMcpError(
|
|
1147
|
+
"invalid_input",
|
|
1148
|
+
`upload_file requires an absolute path; got "${filePath}".`,
|
|
1149
|
+
{ file_path: filePath }
|
|
1150
|
+
);
|
|
1151
|
+
}
|
|
1152
|
+
const locator = this.resolveLocator(s, ref);
|
|
1153
|
+
await locator.setInputFiles(filePath);
|
|
1154
|
+
this.invalidateRefs(s);
|
|
1155
|
+
}
|
|
1156
|
+
// -------------------------------------------------------------------------
|
|
1157
|
+
// v0.5 — web-only extensions (not on Engine interface; tools cast to
|
|
1158
|
+
// PlaywrightEngine before calling).
|
|
1159
|
+
// -------------------------------------------------------------------------
|
|
1160
|
+
/**
|
|
1161
|
+
* Pre-arm a one-shot dialog handler for the next dialog raised on the
|
|
1162
|
+
* active page. Returns when either the dialog fires (and is handled)
|
|
1163
|
+
* or the timeout elapses. The caller is expected to trigger the
|
|
1164
|
+
* dialog (via click etc.) AFTER arming.
|
|
1165
|
+
*/
|
|
1166
|
+
async handleDialog(sessionId, opts) {
|
|
1167
|
+
const s = this.requireSession(sessionId);
|
|
1168
|
+
const timeoutMs = opts.timeoutMs ?? 3e4;
|
|
1169
|
+
const expiresAt = Date.now() + timeoutMs;
|
|
1170
|
+
if (s.dialogArming) {
|
|
1171
|
+
s.dialogArming.resolve(false);
|
|
1172
|
+
}
|
|
1173
|
+
return new Promise((resolve6) => {
|
|
1174
|
+
const arming = {
|
|
1175
|
+
action: opts.action,
|
|
1176
|
+
text: opts.text,
|
|
1177
|
+
expiresAt,
|
|
1178
|
+
resolve: (handled) => {
|
|
1179
|
+
s.dialogArming = null;
|
|
1180
|
+
resolve6({ handled });
|
|
1181
|
+
}
|
|
1182
|
+
};
|
|
1183
|
+
s.dialogArming = arming;
|
|
1184
|
+
const timer = setTimeout(() => {
|
|
1185
|
+
if (s.dialogArming === arming) {
|
|
1186
|
+
s.dialogArming = null;
|
|
1187
|
+
resolve6({ handled: false });
|
|
1188
|
+
}
|
|
1189
|
+
}, timeoutMs);
|
|
1190
|
+
timer.unref?.();
|
|
1191
|
+
});
|
|
1192
|
+
}
|
|
1193
|
+
getConsole(sessionId, opts) {
|
|
1194
|
+
const s = this.requireSession(sessionId);
|
|
1195
|
+
const levels = opts?.levels;
|
|
1196
|
+
const contains = opts?.contains;
|
|
1197
|
+
const limit = opts?.limit ?? 50;
|
|
1198
|
+
let entries = s.consoleBuffer;
|
|
1199
|
+
if (levels && levels.length > 0) {
|
|
1200
|
+
entries = entries.filter((e) => levels.includes(e.level));
|
|
1201
|
+
}
|
|
1202
|
+
if (contains) {
|
|
1203
|
+
entries = entries.filter((e) => e.text.includes(contains));
|
|
1204
|
+
}
|
|
1205
|
+
const result = entries.slice(-limit);
|
|
1206
|
+
if (opts?.clear) s.consoleBuffer = [];
|
|
1207
|
+
return result;
|
|
1208
|
+
}
|
|
1209
|
+
getNetwork(sessionId, opts) {
|
|
1210
|
+
const s = this.requireSession(sessionId);
|
|
1211
|
+
let entries = s.networkBuffer;
|
|
1212
|
+
if (opts?.urlPattern) {
|
|
1213
|
+
if (opts.patternKind === "regex") {
|
|
1214
|
+
const re = new RegExp(opts.urlPattern);
|
|
1215
|
+
entries = entries.filter((e) => re.test(e.url));
|
|
1216
|
+
} else {
|
|
1217
|
+
entries = entries.filter((e) => e.url.includes(opts.urlPattern));
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
1220
|
+
if (opts?.method) {
|
|
1221
|
+
const m = opts.method.toUpperCase();
|
|
1222
|
+
entries = entries.filter((e) => e.method.toUpperCase() === m);
|
|
1223
|
+
}
|
|
1224
|
+
if (opts?.statusRange) {
|
|
1225
|
+
const { min, max } = opts.statusRange;
|
|
1226
|
+
entries = entries.filter(
|
|
1227
|
+
(e) => e.status !== void 0 && e.status >= min && e.status <= max
|
|
1228
|
+
);
|
|
1229
|
+
}
|
|
1230
|
+
if (opts?.onlyFailed) {
|
|
1231
|
+
entries = entries.filter(
|
|
1232
|
+
(e) => !!e.failure || e.status !== void 0 && e.status >= 400
|
|
1233
|
+
);
|
|
1234
|
+
}
|
|
1235
|
+
const limit = opts?.limit ?? 50;
|
|
1236
|
+
const result = entries.slice(-limit);
|
|
1237
|
+
if (opts?.clear) s.networkBuffer = [];
|
|
1238
|
+
return result;
|
|
1239
|
+
}
|
|
1240
|
+
/**
|
|
1241
|
+
* Read the consoleBuffer/networkBuffer directly without filtering —
|
|
1242
|
+
* used by verify_ui_flow expect evaluators.
|
|
1243
|
+
*/
|
|
1244
|
+
peekBuffers(sessionId) {
|
|
1245
|
+
const s = this.requireSession(sessionId);
|
|
1246
|
+
return { console: s.consoleBuffer, network: s.networkBuffer };
|
|
1247
|
+
}
|
|
1248
|
+
/**
|
|
1249
|
+
* Runtime mutation of context-level emulation. CPU + network throttle
|
|
1250
|
+
* use CDP and only work on chromium; everything else is cross-browser.
|
|
1251
|
+
*/
|
|
1252
|
+
async setEnv(sessionId, opts) {
|
|
1253
|
+
const s = this.requireSession(sessionId);
|
|
1254
|
+
const page = this.activePage(s);
|
|
1255
|
+
const ctx = s.session.context;
|
|
1256
|
+
if (opts.viewport) {
|
|
1257
|
+
await page.setViewportSize(opts.viewport);
|
|
1258
|
+
}
|
|
1259
|
+
if (opts.offline !== void 0) {
|
|
1260
|
+
await ctx.setOffline(opts.offline);
|
|
1261
|
+
}
|
|
1262
|
+
if (opts.geolocation) {
|
|
1263
|
+
await ctx.setGeolocation(opts.geolocation);
|
|
1264
|
+
}
|
|
1265
|
+
if (opts.extraHeaders) {
|
|
1266
|
+
await ctx.setExtraHTTPHeaders(opts.extraHeaders);
|
|
1267
|
+
}
|
|
1268
|
+
if (opts.colorScheme || opts.reducedMotion) {
|
|
1269
|
+
await page.emulateMedia({
|
|
1270
|
+
...opts.colorScheme ? { colorScheme: opts.colorScheme } : {},
|
|
1271
|
+
...opts.reducedMotion ? { reducedMotion: opts.reducedMotion } : {}
|
|
1272
|
+
});
|
|
1273
|
+
}
|
|
1274
|
+
if (opts.networkThrottle || opts.cpuThrottle !== void 0) {
|
|
1275
|
+
const browserName = ctx.browser()?.browserType().name();
|
|
1276
|
+
if (browserName !== "chromium") {
|
|
1277
|
+
throw new RolepodMcpError(
|
|
1278
|
+
"unsupported_engine",
|
|
1279
|
+
`networkThrottle / cpuThrottle require chromium (CDP-backed); current browser is "${browserName}".`
|
|
1280
|
+
);
|
|
1281
|
+
}
|
|
1282
|
+
const cdp = await ctx.newCDPSession(page);
|
|
1283
|
+
try {
|
|
1284
|
+
if (opts.networkThrottle) {
|
|
1285
|
+
const preset = NETWORK_PRESETS[opts.networkThrottle];
|
|
1286
|
+
await cdp.send("Network.enable");
|
|
1287
|
+
await cdp.send("Network.emulateNetworkConditions", preset);
|
|
1288
|
+
}
|
|
1289
|
+
if (opts.cpuThrottle !== void 0) {
|
|
1290
|
+
await cdp.send("Emulation.setCPUThrottlingRate", {
|
|
1291
|
+
rate: opts.cpuThrottle
|
|
1292
|
+
});
|
|
1293
|
+
}
|
|
1294
|
+
} finally {
|
|
1295
|
+
await cdp.detach().catch(() => void 0);
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
1298
|
+
this.invalidateRefs(s);
|
|
1299
|
+
}
|
|
1300
|
+
/**
|
|
1301
|
+
* Execute a JavaScript function in the page context. ALWAYS gated by
|
|
1302
|
+
* the tool layer (`ROLEPOD_ALLOW_EVAL=1`); this method does not enforce
|
|
1303
|
+
* the env check.
|
|
1304
|
+
*/
|
|
1305
|
+
async evaluate(sessionId, script, args) {
|
|
1306
|
+
const s = this.requireSession(sessionId);
|
|
1307
|
+
const page = this.activePage(s);
|
|
1308
|
+
return page.evaluate(
|
|
1309
|
+
({ src, a }) => (
|
|
1310
|
+
// eslint-disable-next-line no-new-func
|
|
1311
|
+
new Function("args", `return (async () => { ${src} })();`)(a)
|
|
1312
|
+
),
|
|
1313
|
+
{ src: script, a: args ?? [] }
|
|
1314
|
+
);
|
|
1315
|
+
}
|
|
1316
|
+
listPages(sessionId) {
|
|
1317
|
+
const s = this.requireSession(sessionId);
|
|
1318
|
+
return s.pages.map((p, i) => ({
|
|
1319
|
+
index: i,
|
|
1320
|
+
url: p.url(),
|
|
1321
|
+
title_promise: p.title(),
|
|
1322
|
+
active: i === s.activePageIndex
|
|
1323
|
+
}));
|
|
1324
|
+
}
|
|
1325
|
+
async switchPage(sessionId, index) {
|
|
1326
|
+
const s = this.requireSession(sessionId);
|
|
1327
|
+
if (index < 0 || index >= s.pages.length) {
|
|
1328
|
+
throw new RolepodMcpError(
|
|
1329
|
+
"invalid_input",
|
|
1330
|
+
`Page index ${index} out of range (have ${s.pages.length} page(s)).`,
|
|
1331
|
+
{ index, available: s.pages.length }
|
|
1332
|
+
);
|
|
1333
|
+
}
|
|
1334
|
+
s.activePageIndex = index;
|
|
1335
|
+
this.invalidateRefs(s);
|
|
1336
|
+
}
|
|
1337
|
+
// -------------------------------------------------------------------------
|
|
962
1338
|
// Internal helpers
|
|
963
1339
|
// -------------------------------------------------------------------------
|
|
1340
|
+
activePage(s) {
|
|
1341
|
+
return s.pages[s.activePageIndex] ?? s.session.mainPage;
|
|
1342
|
+
}
|
|
1343
|
+
attachPageListeners(s, page) {
|
|
1344
|
+
page.on("console", (msg) => {
|
|
1345
|
+
const level = mapConsoleLevel(msg.type());
|
|
1346
|
+
pushRing(
|
|
1347
|
+
s.consoleBuffer,
|
|
1348
|
+
{
|
|
1349
|
+
level,
|
|
1350
|
+
text: msg.text(),
|
|
1351
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1352
|
+
location: formatConsoleLocation(msg)
|
|
1353
|
+
},
|
|
1354
|
+
CONSOLE_BUFFER_CAP
|
|
1355
|
+
);
|
|
1356
|
+
});
|
|
1357
|
+
page.on("request", (req) => {
|
|
1358
|
+
const id = s.networkNextId++;
|
|
1359
|
+
s.networkInflight.set(req.url() + "::" + req.method() + "::" + id, {
|
|
1360
|
+
id,
|
|
1361
|
+
startedAt: Date.now(),
|
|
1362
|
+
resourceType: req.resourceType()
|
|
1363
|
+
});
|
|
1364
|
+
pushRing(
|
|
1365
|
+
s.networkBuffer,
|
|
1366
|
+
{
|
|
1367
|
+
id,
|
|
1368
|
+
url: req.url(),
|
|
1369
|
+
method: req.method(),
|
|
1370
|
+
resource_type: req.resourceType(),
|
|
1371
|
+
ts: (/* @__PURE__ */ new Date()).toISOString()
|
|
1372
|
+
},
|
|
1373
|
+
NETWORK_BUFFER_CAP
|
|
1374
|
+
);
|
|
1375
|
+
});
|
|
1376
|
+
page.on("response", (res) => {
|
|
1377
|
+
const req = res.request();
|
|
1378
|
+
const entry = findNetworkEntry(s.networkBuffer, req);
|
|
1379
|
+
if (entry) {
|
|
1380
|
+
entry.status = res.status();
|
|
1381
|
+
entry.duration_ms = Date.now() - new Date(entry.ts).getTime();
|
|
1382
|
+
}
|
|
1383
|
+
});
|
|
1384
|
+
page.on("requestfailed", (req) => {
|
|
1385
|
+
const entry = findNetworkEntry(s.networkBuffer, req);
|
|
1386
|
+
if (entry) {
|
|
1387
|
+
entry.failure = req.failure()?.errorText ?? "request failed";
|
|
1388
|
+
}
|
|
1389
|
+
});
|
|
1390
|
+
page.on("dialog", (dialog) => {
|
|
1391
|
+
void this.handlePageDialog(s, dialog);
|
|
1392
|
+
});
|
|
1393
|
+
}
|
|
1394
|
+
async handlePageDialog(s, dialog) {
|
|
1395
|
+
const arm = s.dialogArming;
|
|
1396
|
+
if (!arm || Date.now() > arm.expiresAt) {
|
|
1397
|
+
await dialog.dismiss().catch(() => void 0);
|
|
1398
|
+
if (arm) arm.resolve(false);
|
|
1399
|
+
return;
|
|
1400
|
+
}
|
|
1401
|
+
try {
|
|
1402
|
+
if (arm.action === "accept") {
|
|
1403
|
+
await dialog.accept();
|
|
1404
|
+
} else if (arm.action === "accept_with_text") {
|
|
1405
|
+
await dialog.accept(arm.text ?? "");
|
|
1406
|
+
} else {
|
|
1407
|
+
await dialog.dismiss();
|
|
1408
|
+
}
|
|
1409
|
+
arm.resolve(true);
|
|
1410
|
+
} catch (err) {
|
|
1411
|
+
log.warn("dialog handle failed", {
|
|
1412
|
+
session_id: s.session.id,
|
|
1413
|
+
err: String(err)
|
|
1414
|
+
});
|
|
1415
|
+
arm.resolve(false);
|
|
1416
|
+
}
|
|
1417
|
+
}
|
|
964
1418
|
requireSession(sessionId) {
|
|
965
1419
|
const s = this.sessions.get(sessionId);
|
|
966
1420
|
if (!s) {
|
|
@@ -989,7 +1443,7 @@ var PlaywrightEngine = class {
|
|
|
989
1443
|
if (meta.ref.startsWith("s")) {
|
|
990
1444
|
throw new UnknownRefError(s.session.id, ref);
|
|
991
1445
|
}
|
|
992
|
-
return s.
|
|
1446
|
+
return this.activePage(s).locator(`aria-ref=${meta.ref}`);
|
|
993
1447
|
}
|
|
994
1448
|
invalidateRefs(s) {
|
|
995
1449
|
s.snapshotGeneration += 1;
|
|
@@ -1125,6 +1579,10 @@ var SessionRegistry = class {
|
|
|
1125
1579
|
}
|
|
1126
1580
|
};
|
|
1127
1581
|
|
|
1582
|
+
// src/tools/composite/verify_ui_flow.ts
|
|
1583
|
+
import { readdir } from "fs/promises";
|
|
1584
|
+
import { resolve as resolvePath2 } from "path";
|
|
1585
|
+
|
|
1128
1586
|
// src/schema/tools.ts
|
|
1129
1587
|
import { z } from "zod";
|
|
1130
1588
|
var platformSchema = z.enum(["web", "ios", "android"]);
|
|
@@ -1228,6 +1686,143 @@ var browserNavigateShape = {
|
|
|
1228
1686
|
url: z.string().url()
|
|
1229
1687
|
};
|
|
1230
1688
|
var browserNavigateSchema = z.object(browserNavigateShape);
|
|
1689
|
+
var browserHoverShape = {
|
|
1690
|
+
session_id: z.string().min(1),
|
|
1691
|
+
ref: z.string().min(1)
|
|
1692
|
+
};
|
|
1693
|
+
var browserHoverSchema = z.object(browserHoverShape);
|
|
1694
|
+
var browserDragShape = {
|
|
1695
|
+
session_id: z.string().min(1),
|
|
1696
|
+
from_ref: z.string().min(1),
|
|
1697
|
+
to_ref: z.string().min(1)
|
|
1698
|
+
};
|
|
1699
|
+
var browserDragSchema = z.object(browserDragShape);
|
|
1700
|
+
var fillFieldKindSchema = z.enum([
|
|
1701
|
+
"input",
|
|
1702
|
+
"select",
|
|
1703
|
+
"checkbox",
|
|
1704
|
+
"radio"
|
|
1705
|
+
]);
|
|
1706
|
+
var fillFormFieldSchema = z.object({
|
|
1707
|
+
ref: z.string().min(1),
|
|
1708
|
+
value: z.union([z.string(), z.boolean()]),
|
|
1709
|
+
kind: fillFieldKindSchema.optional()
|
|
1710
|
+
});
|
|
1711
|
+
var browserFillFormShape = {
|
|
1712
|
+
session_id: z.string().min(1),
|
|
1713
|
+
fields: z.array(fillFormFieldSchema).min(1)
|
|
1714
|
+
};
|
|
1715
|
+
var browserFillFormSchema = z.object(browserFillFormShape);
|
|
1716
|
+
var browserUploadFileShape = {
|
|
1717
|
+
session_id: z.string().min(1),
|
|
1718
|
+
ref: z.string().min(1),
|
|
1719
|
+
file_path: z.string().min(1)
|
|
1720
|
+
};
|
|
1721
|
+
var browserUploadFileSchema = z.object(browserUploadFileShape);
|
|
1722
|
+
var dialogActionSchema = z.enum([
|
|
1723
|
+
"accept",
|
|
1724
|
+
"dismiss",
|
|
1725
|
+
"accept_with_text"
|
|
1726
|
+
]);
|
|
1727
|
+
var browserHandleDialogShape = {
|
|
1728
|
+
session_id: z.string().min(1),
|
|
1729
|
+
action: dialogActionSchema,
|
|
1730
|
+
/** Only used when action='accept_with_text'. */
|
|
1731
|
+
text: z.string().optional(),
|
|
1732
|
+
/**
|
|
1733
|
+
* Arming behavior: registers a one-shot handler for the NEXT dialog
|
|
1734
|
+
* raised on the page. Call this BEFORE the action that triggers the
|
|
1735
|
+
* dialog (e.g. before clicking the button that calls `confirm()`).
|
|
1736
|
+
* Default 30s if no dialog appears, handler is auto-removed.
|
|
1737
|
+
*/
|
|
1738
|
+
timeout_ms: z.number().int().positive().optional()
|
|
1739
|
+
};
|
|
1740
|
+
var browserHandleDialogSchema = z.object(browserHandleDialogShape);
|
|
1741
|
+
var consoleLevelSchema = z.enum([
|
|
1742
|
+
"error",
|
|
1743
|
+
"warning",
|
|
1744
|
+
"info",
|
|
1745
|
+
"log",
|
|
1746
|
+
"debug",
|
|
1747
|
+
"trace"
|
|
1748
|
+
]);
|
|
1749
|
+
var browserConsoleShape = {
|
|
1750
|
+
session_id: z.string().min(1),
|
|
1751
|
+
/** Filter to only these levels. Default: errors+warnings. */
|
|
1752
|
+
levels: z.array(consoleLevelSchema).optional(),
|
|
1753
|
+
/** Substring match on message text. */
|
|
1754
|
+
contains: z.string().optional(),
|
|
1755
|
+
/** Drop all buffered messages after returning. */
|
|
1756
|
+
clear: z.boolean().default(false),
|
|
1757
|
+
/** Cap on returned messages (artifact still holds full ring buffer). */
|
|
1758
|
+
limit: z.number().int().positive().max(1e3).default(50)
|
|
1759
|
+
};
|
|
1760
|
+
var browserConsoleSchema = z.object(browserConsoleShape);
|
|
1761
|
+
var browserNetworkShape = {
|
|
1762
|
+
session_id: z.string().min(1),
|
|
1763
|
+
/** Substring or regex (per `pattern_kind`) match on URL. */
|
|
1764
|
+
url_pattern: z.string().optional(),
|
|
1765
|
+
pattern_kind: z.enum(["substring", "regex"]).default("substring"),
|
|
1766
|
+
method: z.string().optional(),
|
|
1767
|
+
/** Inclusive range — e.g. `{min: 400, max: 599}` for any error response. */
|
|
1768
|
+
status_range: z.object({
|
|
1769
|
+
min: z.number().int().min(100).max(599),
|
|
1770
|
+
max: z.number().int().min(100).max(599)
|
|
1771
|
+
}).optional(),
|
|
1772
|
+
only_failed: z.boolean().default(false),
|
|
1773
|
+
/** Write the full HAR file for this session to artifacts/{runId}/network.har. */
|
|
1774
|
+
export_har: z.boolean().default(false),
|
|
1775
|
+
/** Drop buffered entries after returning. */
|
|
1776
|
+
clear: z.boolean().default(false),
|
|
1777
|
+
limit: z.number().int().positive().max(1e3).default(50)
|
|
1778
|
+
};
|
|
1779
|
+
var browserNetworkSchema = z.object(browserNetworkShape);
|
|
1780
|
+
var networkPresetSchema = z.enum([
|
|
1781
|
+
"offline",
|
|
1782
|
+
"slow-3g",
|
|
1783
|
+
"fast-3g",
|
|
1784
|
+
"slow-4g",
|
|
1785
|
+
"fast-4g",
|
|
1786
|
+
"no-throttling"
|
|
1787
|
+
]);
|
|
1788
|
+
var geolocationSchema = z.object({
|
|
1789
|
+
latitude: z.number().min(-90).max(90),
|
|
1790
|
+
longitude: z.number().min(-180).max(180),
|
|
1791
|
+
accuracy: z.number().nonnegative().optional()
|
|
1792
|
+
});
|
|
1793
|
+
var browserSetEnvShape = {
|
|
1794
|
+
session_id: z.string().min(1),
|
|
1795
|
+
viewport: viewportSchema.optional(),
|
|
1796
|
+
offline: z.boolean().optional(),
|
|
1797
|
+
geolocation: geolocationSchema.optional(),
|
|
1798
|
+
color_scheme: z.enum(["light", "dark", "no-preference"]).optional(),
|
|
1799
|
+
reduced_motion: z.enum(["reduce", "no-preference"]).optional(),
|
|
1800
|
+
extra_headers: z.record(z.string(), z.string()).optional(),
|
|
1801
|
+
network_throttle: networkPresetSchema.optional(),
|
|
1802
|
+
/** CPU slowdown multiplier (1 = no throttle, 4 = 4x slower). Chromium only. */
|
|
1803
|
+
cpu_throttle: z.number().min(1).max(20).optional()
|
|
1804
|
+
};
|
|
1805
|
+
var browserSetEnvSchema = z.object(browserSetEnvShape);
|
|
1806
|
+
var browserEvaluateShape = {
|
|
1807
|
+
session_id: z.string().min(1),
|
|
1808
|
+
script: z.string().min(1),
|
|
1809
|
+
args: z.array(z.unknown()).optional()
|
|
1810
|
+
};
|
|
1811
|
+
var browserEvaluateSchema = z.object(browserEvaluateShape);
|
|
1812
|
+
var browserPagesShape = {
|
|
1813
|
+
session_id: z.string().min(1)
|
|
1814
|
+
};
|
|
1815
|
+
var browserPagesSchema = z.object(browserPagesShape);
|
|
1816
|
+
var browserSwitchPageShape = {
|
|
1817
|
+
session_id: z.string().min(1),
|
|
1818
|
+
index: z.number().int().nonnegative()
|
|
1819
|
+
};
|
|
1820
|
+
var browserSwitchPageSchema = z.object(browserSwitchPageShape);
|
|
1821
|
+
var verifyFillFieldSchema = z.object({
|
|
1822
|
+
query: z.string(),
|
|
1823
|
+
value: z.union([z.string(), z.boolean()]),
|
|
1824
|
+
kind: fillFieldKindSchema.optional()
|
|
1825
|
+
});
|
|
1231
1826
|
var verifyStepSchema = z.discriminatedUnion("kind", [
|
|
1232
1827
|
z.object({ kind: z.literal("click"), query: z.string() }),
|
|
1233
1828
|
z.object({
|
|
@@ -1238,7 +1833,44 @@ var verifyStepSchema = z.discriminatedUnion("kind", [
|
|
|
1238
1833
|
}),
|
|
1239
1834
|
z.object({ kind: z.literal("key"), key: z.string() }),
|
|
1240
1835
|
z.object({ kind: z.literal("wait_for"), condition: waitConditionSchema }),
|
|
1241
|
-
z.object({ kind: z.literal("navigate"), url: z.string().url() })
|
|
1836
|
+
z.object({ kind: z.literal("navigate"), url: z.string().url() }),
|
|
1837
|
+
// v0.5 additions
|
|
1838
|
+
z.object({ kind: z.literal("hover"), query: z.string() }),
|
|
1839
|
+
z.object({
|
|
1840
|
+
kind: z.literal("drag"),
|
|
1841
|
+
from_query: z.string(),
|
|
1842
|
+
to_query: z.string()
|
|
1843
|
+
}),
|
|
1844
|
+
z.object({
|
|
1845
|
+
kind: z.literal("fill_form"),
|
|
1846
|
+
fields: z.array(verifyFillFieldSchema).min(1)
|
|
1847
|
+
}),
|
|
1848
|
+
z.object({
|
|
1849
|
+
kind: z.literal("upload"),
|
|
1850
|
+
query: z.string(),
|
|
1851
|
+
file_path: z.string().min(1)
|
|
1852
|
+
}),
|
|
1853
|
+
z.object({
|
|
1854
|
+
kind: z.literal("dialog"),
|
|
1855
|
+
action: dialogActionSchema,
|
|
1856
|
+
text: z.string().optional()
|
|
1857
|
+
}),
|
|
1858
|
+
z.object({
|
|
1859
|
+
kind: z.literal("set_env"),
|
|
1860
|
+
viewport: viewportSchema.optional(),
|
|
1861
|
+
offline: z.boolean().optional(),
|
|
1862
|
+
geolocation: geolocationSchema.optional(),
|
|
1863
|
+
color_scheme: z.enum(["light", "dark", "no-preference"]).optional(),
|
|
1864
|
+
reduced_motion: z.enum(["reduce", "no-preference"]).optional(),
|
|
1865
|
+
extra_headers: z.record(z.string(), z.string()).optional(),
|
|
1866
|
+
network_throttle: networkPresetSchema.optional(),
|
|
1867
|
+
cpu_throttle: z.number().min(1).max(20).optional()
|
|
1868
|
+
}),
|
|
1869
|
+
z.object({
|
|
1870
|
+
kind: z.literal("switch_page"),
|
|
1871
|
+
index: z.number().int().nonnegative()
|
|
1872
|
+
}),
|
|
1873
|
+
z.object({ kind: z.literal("evaluate"), script: z.string().min(1) })
|
|
1242
1874
|
]);
|
|
1243
1875
|
var verifyExpectSchema = z.discriminatedUnion("kind", [
|
|
1244
1876
|
z.object({ kind: z.literal("text_visible"), text: z.string() }),
|
|
@@ -1248,6 +1880,28 @@ var verifyExpectSchema = z.discriminatedUnion("kind", [
|
|
|
1248
1880
|
kind: z.literal("ref_in_state"),
|
|
1249
1881
|
query: z.string(),
|
|
1250
1882
|
state: z.enum(["visible", "enabled", "focused"])
|
|
1883
|
+
}),
|
|
1884
|
+
// v0.5 additions
|
|
1885
|
+
z.object({
|
|
1886
|
+
kind: z.literal("no_console_errors"),
|
|
1887
|
+
exclude_patterns: z.array(z.string()).optional()
|
|
1888
|
+
}),
|
|
1889
|
+
z.object({
|
|
1890
|
+
kind: z.literal("no_failed_requests"),
|
|
1891
|
+
exclude_patterns: z.array(z.string()).optional(),
|
|
1892
|
+
/** When true, only 5xx counts as a failure. Default false (4xx + 5xx). */
|
|
1893
|
+
allow_4xx: z.boolean().optional()
|
|
1894
|
+
}),
|
|
1895
|
+
z.object({
|
|
1896
|
+
kind: z.literal("request_made"),
|
|
1897
|
+
url_pattern: z.string(),
|
|
1898
|
+
method: z.string().optional(),
|
|
1899
|
+
min_count: z.number().int().positive().optional()
|
|
1900
|
+
}),
|
|
1901
|
+
z.object({
|
|
1902
|
+
kind: z.literal("response_status"),
|
|
1903
|
+
url_pattern: z.string(),
|
|
1904
|
+
status: z.number().int().min(100).max(599)
|
|
1251
1905
|
})
|
|
1252
1906
|
]);
|
|
1253
1907
|
var captureKindSchema = z.enum([
|
|
@@ -1255,7 +1909,8 @@ var captureKindSchema = z.enum([
|
|
|
1255
1909
|
"har",
|
|
1256
1910
|
"console",
|
|
1257
1911
|
"a11y_tree",
|
|
1258
|
-
"video"
|
|
1912
|
+
"video",
|
|
1913
|
+
"trace"
|
|
1259
1914
|
]);
|
|
1260
1915
|
var verifyUiFlowShape = {
|
|
1261
1916
|
mode: z.enum(["assert", "reproduce"]).default("assert"),
|
|
@@ -1333,6 +1988,19 @@ var ToolNames = {
|
|
|
1333
1988
|
browserWaitFor: "rolepod_browser_wait_for",
|
|
1334
1989
|
browserScreenshot: "rolepod_browser_screenshot",
|
|
1335
1990
|
browserNavigate: "rolepod_browser_navigate",
|
|
1991
|
+
// v0.5 atomics
|
|
1992
|
+
browserHover: "rolepod_browser_hover",
|
|
1993
|
+
browserDrag: "rolepod_browser_drag",
|
|
1994
|
+
browserFillForm: "rolepod_browser_fill_form",
|
|
1995
|
+
browserUploadFile: "rolepod_browser_upload_file",
|
|
1996
|
+
browserHandleDialog: "rolepod_browser_handle_dialog",
|
|
1997
|
+
browserConsole: "rolepod_browser_console",
|
|
1998
|
+
browserNetwork: "rolepod_browser_network",
|
|
1999
|
+
browserSetEnv: "rolepod_browser_set_env",
|
|
2000
|
+
browserEvaluate: "rolepod_browser_evaluate",
|
|
2001
|
+
browserPages: "rolepod_browser_pages",
|
|
2002
|
+
browserSwitchPage: "rolepod_browser_switch_page",
|
|
2003
|
+
// composite
|
|
1336
2004
|
verifyUiFlow: "rolepod_verify_ui_flow",
|
|
1337
2005
|
auditA11y: "rolepod_audit_a11y",
|
|
1338
2006
|
visualDiff: "rolepod_visual_diff",
|
|
@@ -1437,13 +2105,32 @@ var verifyUiFlowTool = {
|
|
|
1437
2105
|
});
|
|
1438
2106
|
}
|
|
1439
2107
|
};
|
|
2108
|
+
function buildCaptureOptions(captures, runDir) {
|
|
2109
|
+
const cap = {};
|
|
2110
|
+
if (captures.has("har")) {
|
|
2111
|
+
cap.har = { path: resolvePath2(runDir, "network.har") };
|
|
2112
|
+
}
|
|
2113
|
+
if (captures.has("video")) {
|
|
2114
|
+
cap.video = { dir: resolvePath2(runDir, "videos") };
|
|
2115
|
+
}
|
|
2116
|
+
if (captures.has("trace")) {
|
|
2117
|
+
cap.trace = { artifactDir: runDir };
|
|
2118
|
+
}
|
|
2119
|
+
return Object.keys(cap).length > 0 ? cap : void 0;
|
|
2120
|
+
}
|
|
1440
2121
|
async function runFlow(ctx, args, steps, runDir, opts) {
|
|
1441
2122
|
const evidence = { screenshots: [] };
|
|
2123
|
+
const captures = new Set(args.capture ?? ["screenshot"]);
|
|
1442
2124
|
let passed = false;
|
|
1443
2125
|
let failedAtStep;
|
|
1444
2126
|
let failureReason;
|
|
1445
2127
|
let finalSnapshot;
|
|
1446
|
-
const
|
|
2128
|
+
const openOpts = { ...args.open };
|
|
2129
|
+
const captureCfg = buildCaptureOptions(captures, runDir);
|
|
2130
|
+
if (captureCfg) {
|
|
2131
|
+
openOpts.capture = captureCfg;
|
|
2132
|
+
}
|
|
2133
|
+
const session = await ctx.registry.open(openOpts);
|
|
1447
2134
|
const engine = ctx.registry.engineFor(session.id);
|
|
1448
2135
|
const sessionHandle = { id: session.id, platform: session.platform };
|
|
1449
2136
|
try {
|
|
@@ -1462,7 +2149,7 @@ async function runFlow(ctx, args, steps, runDir, opts) {
|
|
|
1462
2149
|
const failures = [];
|
|
1463
2150
|
for (let i = 0; i < args.expect.length; i++) {
|
|
1464
2151
|
const expectation = args.expect[i];
|
|
1465
|
-
if (!evaluateExpect(expectation, finalSnapshot)) {
|
|
2152
|
+
if (!evaluateExpect(expectation, finalSnapshot, engine, session.id)) {
|
|
1466
2153
|
failures.push(`expect[${i}] ${describeExpect(expectation)}`);
|
|
1467
2154
|
}
|
|
1468
2155
|
}
|
|
@@ -1476,8 +2163,7 @@ async function runFlow(ctx, args, steps, runDir, opts) {
|
|
|
1476
2163
|
passed = false;
|
|
1477
2164
|
} finally {
|
|
1478
2165
|
if (opts.captureEvidence) {
|
|
1479
|
-
|
|
1480
|
-
if (wantScreenshot) {
|
|
2166
|
+
if (captures.has("screenshot")) {
|
|
1481
2167
|
try {
|
|
1482
2168
|
const buf = await engine.screenshot(sessionHandle, true);
|
|
1483
2169
|
const p = await ctx.store.writeScreenshot(runDir, buf, "final");
|
|
@@ -1486,6 +2172,37 @@ async function runFlow(ctx, args, steps, runDir, opts) {
|
|
|
1486
2172
|
failureReason ??= `screenshot capture failed: ${describeError(err)}`;
|
|
1487
2173
|
}
|
|
1488
2174
|
}
|
|
2175
|
+
if (captures.has("console") && engine instanceof PlaywrightEngine) {
|
|
2176
|
+
try {
|
|
2177
|
+
const messages = engine.peekBuffers(session.id).console;
|
|
2178
|
+
evidence.console = await ctx.store.writeReport(
|
|
2179
|
+
runDir,
|
|
2180
|
+
"console.json",
|
|
2181
|
+
JSON.stringify(
|
|
2182
|
+
{
|
|
2183
|
+
count: messages.length,
|
|
2184
|
+
by_level: countByLevel(messages),
|
|
2185
|
+
messages
|
|
2186
|
+
},
|
|
2187
|
+
null,
|
|
2188
|
+
2
|
|
2189
|
+
)
|
|
2190
|
+
);
|
|
2191
|
+
} catch (err) {
|
|
2192
|
+
failureReason ??= `console capture failed: ${describeError(err)}`;
|
|
2193
|
+
}
|
|
2194
|
+
}
|
|
2195
|
+
if (captures.has("a11y_tree") && finalSnapshot) {
|
|
2196
|
+
try {
|
|
2197
|
+
evidence.a11y_tree = await ctx.store.writeReport(
|
|
2198
|
+
runDir,
|
|
2199
|
+
"a11y_tree.json",
|
|
2200
|
+
JSON.stringify(finalSnapshot, null, 2)
|
|
2201
|
+
);
|
|
2202
|
+
} catch (err) {
|
|
2203
|
+
failureReason ??= `a11y_tree capture failed: ${describeError(err)}`;
|
|
2204
|
+
}
|
|
2205
|
+
}
|
|
1489
2206
|
try {
|
|
1490
2207
|
evidence.replay_bundle = await ctx.store.writeReplayBundle(
|
|
1491
2208
|
runDir,
|
|
@@ -1506,12 +2223,32 @@ async function runFlow(ctx, args, steps, runDir, opts) {
|
|
|
1506
2223
|
await ctx.registry.close(sessionHandle).catch(() => void 0);
|
|
1507
2224
|
}
|
|
1508
2225
|
}
|
|
2226
|
+
if (opts.captureEvidence) {
|
|
2227
|
+
if (captureCfg?.har) evidence.har = captureCfg.har.path;
|
|
2228
|
+
if (captureCfg?.trace) {
|
|
2229
|
+
evidence.trace = resolvePath2(captureCfg.trace.artifactDir, "trace.zip");
|
|
2230
|
+
}
|
|
2231
|
+
if (captureCfg?.video) {
|
|
2232
|
+
try {
|
|
2233
|
+
const files = await readdir(captureCfg.video.dir).catch(() => []);
|
|
2234
|
+
evidence.video = files.filter((f) => f.endsWith(".webm")).map((f) => resolvePath2(captureCfg.video.dir, f));
|
|
2235
|
+
} catch {
|
|
2236
|
+
}
|
|
2237
|
+
}
|
|
2238
|
+
}
|
|
1509
2239
|
const out = { passed, evidence };
|
|
1510
2240
|
if (failedAtStep !== void 0) out.failedAtStep = failedAtStep;
|
|
1511
2241
|
if (failureReason !== void 0) out.failureReason = failureReason;
|
|
1512
2242
|
if (finalSnapshot) out.finalUrl = finalSnapshot.url_or_screen;
|
|
1513
2243
|
return out;
|
|
1514
2244
|
}
|
|
2245
|
+
function countByLevel(messages) {
|
|
2246
|
+
const counts = {};
|
|
2247
|
+
for (const m of messages) {
|
|
2248
|
+
counts[m.level] = (counts[m.level] ?? 0) + 1;
|
|
2249
|
+
}
|
|
2250
|
+
return counts;
|
|
2251
|
+
}
|
|
1515
2252
|
async function minimize(ctx, args, initialSteps, runDir) {
|
|
1516
2253
|
const tagged = initialSteps.map((step, origIndex) => ({ step, origIndex }));
|
|
1517
2254
|
let attempts = 0;
|
|
@@ -1575,9 +2312,84 @@ async function runStep(engine, session, step, snap) {
|
|
|
1575
2312
|
case "navigate":
|
|
1576
2313
|
await engine.navigate(session, step.url);
|
|
1577
2314
|
return;
|
|
2315
|
+
case "hover": {
|
|
2316
|
+
const ref = findRefByQuery(snap.tree, step.query);
|
|
2317
|
+
if (!ref) throw missingQuery(step.query);
|
|
2318
|
+
await engine.hover(session, ref);
|
|
2319
|
+
return;
|
|
2320
|
+
}
|
|
2321
|
+
case "drag": {
|
|
2322
|
+
const fromRef = findRefByQuery(snap.tree, step.from_query);
|
|
2323
|
+
if (!fromRef) throw missingQuery(step.from_query);
|
|
2324
|
+
const toRef = findRefByQuery(snap.tree, step.to_query);
|
|
2325
|
+
if (!toRef) throw missingQuery(step.to_query);
|
|
2326
|
+
await engine.drag(session, fromRef, toRef);
|
|
2327
|
+
return;
|
|
2328
|
+
}
|
|
2329
|
+
case "fill_form": {
|
|
2330
|
+
const resolved = step.fields.map((f) => {
|
|
2331
|
+
const ref = findRefByQuery(snap.tree, f.query);
|
|
2332
|
+
if (!ref) throw missingQuery(f.query);
|
|
2333
|
+
return f.kind !== void 0 ? { ref, value: f.value, kind: f.kind } : { ref, value: f.value };
|
|
2334
|
+
});
|
|
2335
|
+
await engine.fillForm(session, resolved);
|
|
2336
|
+
return;
|
|
2337
|
+
}
|
|
2338
|
+
case "upload": {
|
|
2339
|
+
const ref = findRefByQuery(snap.tree, step.query);
|
|
2340
|
+
if (!ref) throw missingQuery(step.query);
|
|
2341
|
+
await engine.uploadFile(session, ref, step.file_path);
|
|
2342
|
+
return;
|
|
2343
|
+
}
|
|
2344
|
+
case "dialog": {
|
|
2345
|
+
requirePlaywright(engine, "dialog");
|
|
2346
|
+
void engine.handleDialog(session.id, {
|
|
2347
|
+
action: step.action,
|
|
2348
|
+
...step.text !== void 0 ? { text: step.text } : {}
|
|
2349
|
+
}).catch(() => void 0);
|
|
2350
|
+
return;
|
|
2351
|
+
}
|
|
2352
|
+
case "set_env": {
|
|
2353
|
+
requirePlaywright(engine, "set_env");
|
|
2354
|
+
await engine.setEnv(session.id, {
|
|
2355
|
+
...step.viewport !== void 0 ? { viewport: step.viewport } : {},
|
|
2356
|
+
...step.offline !== void 0 ? { offline: step.offline } : {},
|
|
2357
|
+
...step.geolocation !== void 0 ? { geolocation: step.geolocation } : {},
|
|
2358
|
+
...step.color_scheme !== void 0 ? { colorScheme: step.color_scheme } : {},
|
|
2359
|
+
...step.reduced_motion !== void 0 ? { reducedMotion: step.reduced_motion } : {},
|
|
2360
|
+
...step.extra_headers !== void 0 ? { extraHeaders: step.extra_headers } : {},
|
|
2361
|
+
...step.network_throttle !== void 0 ? { networkThrottle: step.network_throttle } : {},
|
|
2362
|
+
...step.cpu_throttle !== void 0 ? { cpuThrottle: step.cpu_throttle } : {}
|
|
2363
|
+
});
|
|
2364
|
+
return;
|
|
2365
|
+
}
|
|
2366
|
+
case "switch_page": {
|
|
2367
|
+
requirePlaywright(engine, "switch_page");
|
|
2368
|
+
await engine.switchPage(session.id, step.index);
|
|
2369
|
+
return;
|
|
2370
|
+
}
|
|
2371
|
+
case "evaluate": {
|
|
2372
|
+
requirePlaywright(engine, "evaluate");
|
|
2373
|
+
if (process.env.ROLEPOD_ALLOW_EVAL !== "1") {
|
|
2374
|
+
throw new RolepodMcpError(
|
|
2375
|
+
"engine_error",
|
|
2376
|
+
"verify_ui_flow step kind 'evaluate' is disabled. Restart the rolepod-uiproof MCP server with ROLEPOD_ALLOW_EVAL=1 to enable."
|
|
2377
|
+
);
|
|
2378
|
+
}
|
|
2379
|
+
await engine.evaluate(session.id, step.script);
|
|
2380
|
+
return;
|
|
2381
|
+
}
|
|
2382
|
+
}
|
|
2383
|
+
}
|
|
2384
|
+
function requirePlaywright(engine, stepKind) {
|
|
2385
|
+
if (!(engine instanceof PlaywrightEngine)) {
|
|
2386
|
+
throw new RolepodMcpError(
|
|
2387
|
+
"unsupported_engine",
|
|
2388
|
+
`verify_ui_flow step kind "${stepKind}" is web-only and requires PlaywrightEngine.`
|
|
2389
|
+
);
|
|
1578
2390
|
}
|
|
1579
2391
|
}
|
|
1580
|
-
function evaluateExpect(exp, snap) {
|
|
2392
|
+
function evaluateExpect(exp, snap, engine, sessionId) {
|
|
1581
2393
|
switch (exp.kind) {
|
|
1582
2394
|
case "text_visible":
|
|
1583
2395
|
return treeHasText(snap.tree, exp.text);
|
|
@@ -1597,6 +2409,49 @@ function evaluateExpect(exp, snap) {
|
|
|
1597
2409
|
return node.state?.focused === true;
|
|
1598
2410
|
}
|
|
1599
2411
|
}
|
|
2412
|
+
case "no_console_errors": {
|
|
2413
|
+
if (!(engine instanceof PlaywrightEngine)) return true;
|
|
2414
|
+
const msgs = engine.peekBuffers(sessionId).console.filter(
|
|
2415
|
+
(m) => m.level === "error"
|
|
2416
|
+
);
|
|
2417
|
+
const excludes = exp.exclude_patterns ?? [];
|
|
2418
|
+
const remaining = msgs.filter(
|
|
2419
|
+
(m) => !excludes.some((p) => m.text.includes(p))
|
|
2420
|
+
);
|
|
2421
|
+
return remaining.length === 0;
|
|
2422
|
+
}
|
|
2423
|
+
case "no_failed_requests": {
|
|
2424
|
+
if (!(engine instanceof PlaywrightEngine)) return true;
|
|
2425
|
+
const reqs = engine.peekBuffers(sessionId).network.filter((r) => {
|
|
2426
|
+
if (r.failure) return true;
|
|
2427
|
+
if (r.status === void 0) return false;
|
|
2428
|
+
if (exp.allow_4xx) return r.status >= 500;
|
|
2429
|
+
return r.status >= 400;
|
|
2430
|
+
});
|
|
2431
|
+
const excludes = exp.exclude_patterns ?? [];
|
|
2432
|
+
const remaining = reqs.filter(
|
|
2433
|
+
(r) => !excludes.some((p) => r.url.includes(p))
|
|
2434
|
+
);
|
|
2435
|
+
return remaining.length === 0;
|
|
2436
|
+
}
|
|
2437
|
+
case "request_made": {
|
|
2438
|
+
if (!(engine instanceof PlaywrightEngine)) return false;
|
|
2439
|
+
const re = new RegExp(exp.url_pattern);
|
|
2440
|
+
const wantMethod = exp.method?.toUpperCase();
|
|
2441
|
+
const matches = engine.peekBuffers(sessionId).network.filter((r) => {
|
|
2442
|
+
if (!re.test(r.url)) return false;
|
|
2443
|
+
if (wantMethod && r.method.toUpperCase() !== wantMethod) return false;
|
|
2444
|
+
return true;
|
|
2445
|
+
});
|
|
2446
|
+
const min = exp.min_count ?? 1;
|
|
2447
|
+
return matches.length >= min;
|
|
2448
|
+
}
|
|
2449
|
+
case "response_status": {
|
|
2450
|
+
if (!(engine instanceof PlaywrightEngine)) return false;
|
|
2451
|
+
const re = new RegExp(exp.url_pattern);
|
|
2452
|
+
const match = engine.peekBuffers(sessionId).network.find((r) => re.test(r.url) && r.status === exp.status);
|
|
2453
|
+
return match !== void 0;
|
|
2454
|
+
}
|
|
1600
2455
|
}
|
|
1601
2456
|
}
|
|
1602
2457
|
function describeExpect(exp) {
|
|
@@ -1609,6 +2464,14 @@ function describeExpect(exp) {
|
|
|
1609
2464
|
return `url_matches /${exp.pattern}/`;
|
|
1610
2465
|
case "ref_in_state":
|
|
1611
2466
|
return `ref_in_state "${exp.query}" \u2192 ${exp.state}`;
|
|
2467
|
+
case "no_console_errors":
|
|
2468
|
+
return "no_console_errors";
|
|
2469
|
+
case "no_failed_requests":
|
|
2470
|
+
return "no_failed_requests";
|
|
2471
|
+
case "request_made":
|
|
2472
|
+
return `request_made ${exp.method ?? ""} ${exp.url_pattern}`.trim();
|
|
2473
|
+
case "response_status":
|
|
2474
|
+
return `response_status ${exp.url_pattern} = ${exp.status}`;
|
|
1612
2475
|
}
|
|
1613
2476
|
}
|
|
1614
2477
|
function missingQuery(query) {
|
|
@@ -1725,6 +2588,151 @@ var browserCloseTool = {
|
|
|
1725
2588
|
}
|
|
1726
2589
|
};
|
|
1727
2590
|
|
|
2591
|
+
// src/tools/atomic/browser_console.ts
|
|
2592
|
+
var browserConsoleTool = {
|
|
2593
|
+
name: ToolNames.browserConsole,
|
|
2594
|
+
description: "List console messages emitted by the active page since the session opened (or since the last `clear`). Filters: `levels` (default: errors+warnings), substring `contains`, and `limit` (default 50, max 1000). Set `clear: true` to drain the buffer after returning. Read-only.",
|
|
2595
|
+
inputShape: browserConsoleShape,
|
|
2596
|
+
build(ctx) {
|
|
2597
|
+
return safeHandler(async (args) => {
|
|
2598
|
+
const engine = ctx.registry.engineFor(args.session_id);
|
|
2599
|
+
if (!(engine instanceof PlaywrightEngine)) {
|
|
2600
|
+
throw new RolepodMcpError(
|
|
2601
|
+
"unsupported_engine",
|
|
2602
|
+
"console is web-only and requires PlaywrightEngine."
|
|
2603
|
+
);
|
|
2604
|
+
}
|
|
2605
|
+
const levels = args.levels && args.levels.length > 0 ? args.levels : ["error", "warning"];
|
|
2606
|
+
const messages = engine.getConsole(args.session_id, {
|
|
2607
|
+
levels,
|
|
2608
|
+
...args.contains !== void 0 ? { contains: args.contains } : {},
|
|
2609
|
+
clear: args.clear,
|
|
2610
|
+
limit: args.limit
|
|
2611
|
+
});
|
|
2612
|
+
return ok({
|
|
2613
|
+
count: messages.length,
|
|
2614
|
+
messages
|
|
2615
|
+
});
|
|
2616
|
+
});
|
|
2617
|
+
}
|
|
2618
|
+
};
|
|
2619
|
+
|
|
2620
|
+
// src/tools/atomic/browser_drag.ts
|
|
2621
|
+
var browserDragTool = {
|
|
2622
|
+
name: ToolNames.browserDrag,
|
|
2623
|
+
description: "Drag the element identified by `from_ref` onto the element identified by `to_ref`. Both refs come from the most recent snapshot. Invalidates all refs on success.",
|
|
2624
|
+
inputShape: browserDragShape,
|
|
2625
|
+
build(ctx) {
|
|
2626
|
+
return safeHandler(async (args) => {
|
|
2627
|
+
const engine = ctx.registry.engineFor(args.session_id);
|
|
2628
|
+
await engine.drag(
|
|
2629
|
+
{
|
|
2630
|
+
id: args.session_id,
|
|
2631
|
+
platform: ctx.registry.platformOf(args.session_id)
|
|
2632
|
+
},
|
|
2633
|
+
args.from_ref,
|
|
2634
|
+
args.to_ref
|
|
2635
|
+
);
|
|
2636
|
+
return ok({ dragged: true });
|
|
2637
|
+
});
|
|
2638
|
+
}
|
|
2639
|
+
};
|
|
2640
|
+
|
|
2641
|
+
// src/tools/atomic/browser_evaluate.ts
|
|
2642
|
+
var browserEvaluateTool = {
|
|
2643
|
+
name: ToolNames.browserEvaluate,
|
|
2644
|
+
description: "Execute JavaScript in the active page's context. The `script` is the body of an async function \u2014 use `return` for the result and reference inputs via the implicit `args` array. DISABLED unless the MCP server was launched with `ROLEPOD_ALLOW_EVAL=1`; intended for trusted automation setups only (state seeding, computed-style reads, synthetic event dispatch).",
|
|
2645
|
+
inputShape: browserEvaluateShape,
|
|
2646
|
+
build(ctx) {
|
|
2647
|
+
const allowed = process.env.ROLEPOD_ALLOW_EVAL === "1";
|
|
2648
|
+
return safeHandler(async (args) => {
|
|
2649
|
+
if (!allowed) {
|
|
2650
|
+
throw new RolepodMcpError(
|
|
2651
|
+
"engine_error",
|
|
2652
|
+
"browser_evaluate is disabled. Restart the rolepod-uiproof MCP server with the env var ROLEPOD_ALLOW_EVAL=1 to enable arbitrary JavaScript execution in the page context."
|
|
2653
|
+
);
|
|
2654
|
+
}
|
|
2655
|
+
const engine = ctx.registry.engineFor(args.session_id);
|
|
2656
|
+
if (!(engine instanceof PlaywrightEngine)) {
|
|
2657
|
+
throw new RolepodMcpError(
|
|
2658
|
+
"unsupported_engine",
|
|
2659
|
+
"evaluate is web-only and requires PlaywrightEngine."
|
|
2660
|
+
);
|
|
2661
|
+
}
|
|
2662
|
+
const result = await engine.evaluate(
|
|
2663
|
+
args.session_id,
|
|
2664
|
+
args.script,
|
|
2665
|
+
args.args
|
|
2666
|
+
);
|
|
2667
|
+
return ok({ result });
|
|
2668
|
+
});
|
|
2669
|
+
}
|
|
2670
|
+
};
|
|
2671
|
+
|
|
2672
|
+
// src/tools/atomic/browser_fill_form.ts
|
|
2673
|
+
var browserFillFormTool = {
|
|
2674
|
+
name: ToolNames.browserFillForm,
|
|
2675
|
+
description: "Batch-fill multiple form fields (inputs, selects, checkboxes, radios) in one call. Each field needs a `ref` from the latest snapshot and a `value`; pass `kind` to disambiguate non-input controls. Token-efficient alternative to a sequence of `type` calls. Invalidates all refs on success.",
|
|
2676
|
+
inputShape: browserFillFormShape,
|
|
2677
|
+
build(ctx) {
|
|
2678
|
+
return safeHandler(async (args) => {
|
|
2679
|
+
const engine = ctx.registry.engineFor(args.session_id);
|
|
2680
|
+
await engine.fillForm(
|
|
2681
|
+
{
|
|
2682
|
+
id: args.session_id,
|
|
2683
|
+
platform: ctx.registry.platformOf(args.session_id)
|
|
2684
|
+
},
|
|
2685
|
+
args.fields
|
|
2686
|
+
);
|
|
2687
|
+
return ok({ filled: args.fields.length });
|
|
2688
|
+
});
|
|
2689
|
+
}
|
|
2690
|
+
};
|
|
2691
|
+
|
|
2692
|
+
// src/tools/atomic/browser_handle_dialog.ts
|
|
2693
|
+
var browserHandleDialogTool = {
|
|
2694
|
+
name: ToolNames.browserHandleDialog,
|
|
2695
|
+
description: "Pre-arm a one-shot handler for the NEXT JavaScript dialog (`alert`/`confirm`/`prompt`/`beforeunload`) on the active page. Call this BEFORE the action that triggers the dialog (e.g. before clicking the button that calls `confirm()`). Returns when the dialog fires or the timeout (default 30s) elapses. Un-armed dialogs are auto-dismissed so the page does not hang.",
|
|
2696
|
+
inputShape: browserHandleDialogShape,
|
|
2697
|
+
build(ctx) {
|
|
2698
|
+
return safeHandler(async (args) => {
|
|
2699
|
+
const engine = ctx.registry.engineFor(args.session_id);
|
|
2700
|
+
if (!(engine instanceof PlaywrightEngine)) {
|
|
2701
|
+
throw new RolepodMcpError(
|
|
2702
|
+
"unsupported_engine",
|
|
2703
|
+
"handle_dialog is web-only and requires PlaywrightEngine."
|
|
2704
|
+
);
|
|
2705
|
+
}
|
|
2706
|
+
const { handled } = await engine.handleDialog(args.session_id, {
|
|
2707
|
+
action: args.action,
|
|
2708
|
+
...args.text !== void 0 ? { text: args.text } : {},
|
|
2709
|
+
...args.timeout_ms !== void 0 ? { timeoutMs: args.timeout_ms } : {}
|
|
2710
|
+
});
|
|
2711
|
+
return ok({ handled, action: args.action });
|
|
2712
|
+
});
|
|
2713
|
+
}
|
|
2714
|
+
};
|
|
2715
|
+
|
|
2716
|
+
// src/tools/atomic/browser_hover.ts
|
|
2717
|
+
var browserHoverTool = {
|
|
2718
|
+
name: ToolNames.browserHover,
|
|
2719
|
+
description: "Hover the pointer over the element identified by `ref`. Refs remain valid afterwards (read-mostly).",
|
|
2720
|
+
inputShape: browserHoverShape,
|
|
2721
|
+
build(ctx) {
|
|
2722
|
+
return safeHandler(async (args) => {
|
|
2723
|
+
const engine = ctx.registry.engineFor(args.session_id);
|
|
2724
|
+
await engine.hover(
|
|
2725
|
+
{
|
|
2726
|
+
id: args.session_id,
|
|
2727
|
+
platform: ctx.registry.platformOf(args.session_id)
|
|
2728
|
+
},
|
|
2729
|
+
args.ref
|
|
2730
|
+
);
|
|
2731
|
+
return ok({ hovered: true });
|
|
2732
|
+
});
|
|
2733
|
+
}
|
|
2734
|
+
};
|
|
2735
|
+
|
|
1728
2736
|
// src/tools/atomic/browser_key.ts
|
|
1729
2737
|
var browserKeyTool = {
|
|
1730
2738
|
name: ToolNames.browserKey,
|
|
@@ -1753,6 +2761,46 @@ var browserNavigateTool = {
|
|
|
1753
2761
|
}
|
|
1754
2762
|
};
|
|
1755
2763
|
|
|
2764
|
+
// src/tools/atomic/browser_network.ts
|
|
2765
|
+
var browserNetworkTool = {
|
|
2766
|
+
name: ToolNames.browserNetwork,
|
|
2767
|
+
description: 'List network requests captured on the active page. Filters: `url_pattern` (substring or regex via `pattern_kind`), `method`, `status_range`, `only_failed`. Set `export_har: true` to require the session to have been opened with HAR recording \u2014 see `verify_ui_flow` capture=["har"] or call `browser_open` with capture.har=true. Read-only unless `clear: true`.',
|
|
2768
|
+
inputShape: browserNetworkShape,
|
|
2769
|
+
build(ctx) {
|
|
2770
|
+
return safeHandler(async (args) => {
|
|
2771
|
+
const engine = ctx.registry.engineFor(args.session_id);
|
|
2772
|
+
if (!(engine instanceof PlaywrightEngine)) {
|
|
2773
|
+
throw new RolepodMcpError(
|
|
2774
|
+
"unsupported_engine",
|
|
2775
|
+
"network is web-only and requires PlaywrightEngine."
|
|
2776
|
+
);
|
|
2777
|
+
}
|
|
2778
|
+
const requests = engine.getNetwork(args.session_id, {
|
|
2779
|
+
...args.url_pattern !== void 0 ? { urlPattern: args.url_pattern } : {},
|
|
2780
|
+
patternKind: args.pattern_kind,
|
|
2781
|
+
...args.method !== void 0 ? { method: args.method } : {},
|
|
2782
|
+
...args.status_range !== void 0 ? { statusRange: args.status_range } : {},
|
|
2783
|
+
onlyFailed: args.only_failed,
|
|
2784
|
+
clear: args.clear,
|
|
2785
|
+
limit: args.limit
|
|
2786
|
+
});
|
|
2787
|
+
const failed = requests.filter(
|
|
2788
|
+
(r) => !!r.failure || r.status !== void 0 && r.status >= 400
|
|
2789
|
+
).length;
|
|
2790
|
+
return ok({
|
|
2791
|
+
count: requests.length,
|
|
2792
|
+
failed_count: failed,
|
|
2793
|
+
requests,
|
|
2794
|
+
// HAR file lives wherever the session was opened with
|
|
2795
|
+
// `capture.har.path`. We don't echo it here to avoid leaking
|
|
2796
|
+
// filesystem paths into untrusted logs; the verify_ui_flow run
|
|
2797
|
+
// result surfaces it in `evidence_paths.har`.
|
|
2798
|
+
har_recording: args.export_har ? "HAR is written at session close to the path passed via capture.har at open time." : void 0
|
|
2799
|
+
});
|
|
2800
|
+
});
|
|
2801
|
+
}
|
|
2802
|
+
};
|
|
2803
|
+
|
|
1756
2804
|
// src/tools/atomic/browser_open.ts
|
|
1757
2805
|
var browserOpenTool = {
|
|
1758
2806
|
name: ToolNames.browserOpen,
|
|
@@ -1766,6 +2814,34 @@ var browserOpenTool = {
|
|
|
1766
2814
|
}
|
|
1767
2815
|
};
|
|
1768
2816
|
|
|
2817
|
+
// src/tools/atomic/browser_pages.ts
|
|
2818
|
+
var browserPagesTool = {
|
|
2819
|
+
name: ToolNames.browserPages,
|
|
2820
|
+
description: "List all pages currently open in the session's browser context \u2014 typically just the main page, plus any popups, OAuth windows, or `target=_blank` tabs that the page itself opened. Each entry carries `{ index, url, title, active }`. Read-only.",
|
|
2821
|
+
inputShape: browserPagesShape,
|
|
2822
|
+
build(ctx) {
|
|
2823
|
+
return safeHandler(async (args) => {
|
|
2824
|
+
const engine = ctx.registry.engineFor(args.session_id);
|
|
2825
|
+
if (!(engine instanceof PlaywrightEngine)) {
|
|
2826
|
+
throw new RolepodMcpError(
|
|
2827
|
+
"unsupported_engine",
|
|
2828
|
+
"pages is web-only and requires PlaywrightEngine."
|
|
2829
|
+
);
|
|
2830
|
+
}
|
|
2831
|
+
const raw = engine.listPages(args.session_id);
|
|
2832
|
+
const pages = await Promise.all(
|
|
2833
|
+
raw.map(async (p) => ({
|
|
2834
|
+
index: p.index,
|
|
2835
|
+
url: p.url,
|
|
2836
|
+
title: await p.title_promise.catch(() => ""),
|
|
2837
|
+
active: p.active
|
|
2838
|
+
}))
|
|
2839
|
+
);
|
|
2840
|
+
return ok({ count: pages.length, pages });
|
|
2841
|
+
});
|
|
2842
|
+
}
|
|
2843
|
+
};
|
|
2844
|
+
|
|
1769
2845
|
// src/tools/atomic/browser_screenshot.ts
|
|
1770
2846
|
var browserScreenshotTool = {
|
|
1771
2847
|
name: ToolNames.browserScreenshot,
|
|
@@ -1810,6 +2886,35 @@ var browserScrollTool = {
|
|
|
1810
2886
|
}
|
|
1811
2887
|
};
|
|
1812
2888
|
|
|
2889
|
+
// src/tools/atomic/browser_set_env.ts
|
|
2890
|
+
var browserSetEnvTool = {
|
|
2891
|
+
name: ToolNames.browserSetEnv,
|
|
2892
|
+
description: "Mutate session environment at runtime: viewport, offline state, geolocation, color_scheme (`light`/`dark`), reduced_motion, extra HTTP headers, network throttle preset (`slow-3g`/`fast-3g`/`slow-4g`/`fast-4g`/`offline`/`no-throttling`), and CPU throttle multiplier. `network_throttle` and `cpu_throttle` are chromium-only (CDP). `user_agent`, `locale`, and `timezone` cannot be changed mid-session \u2014 set them at `browser_open` time. Invalidates all refs.",
|
|
2893
|
+
inputShape: browserSetEnvShape,
|
|
2894
|
+
build(ctx) {
|
|
2895
|
+
return safeHandler(async (args) => {
|
|
2896
|
+
const engine = ctx.registry.engineFor(args.session_id);
|
|
2897
|
+
if (!(engine instanceof PlaywrightEngine)) {
|
|
2898
|
+
throw new RolepodMcpError(
|
|
2899
|
+
"unsupported_engine",
|
|
2900
|
+
"set_env is web-only and requires PlaywrightEngine."
|
|
2901
|
+
);
|
|
2902
|
+
}
|
|
2903
|
+
await engine.setEnv(args.session_id, {
|
|
2904
|
+
...args.viewport !== void 0 ? { viewport: args.viewport } : {},
|
|
2905
|
+
...args.offline !== void 0 ? { offline: args.offline } : {},
|
|
2906
|
+
...args.geolocation !== void 0 ? { geolocation: args.geolocation } : {},
|
|
2907
|
+
...args.color_scheme !== void 0 ? { colorScheme: args.color_scheme } : {},
|
|
2908
|
+
...args.reduced_motion !== void 0 ? { reducedMotion: args.reduced_motion } : {},
|
|
2909
|
+
...args.extra_headers !== void 0 ? { extraHeaders: args.extra_headers } : {},
|
|
2910
|
+
...args.network_throttle !== void 0 ? { networkThrottle: args.network_throttle } : {},
|
|
2911
|
+
...args.cpu_throttle !== void 0 ? { cpuThrottle: args.cpu_throttle } : {}
|
|
2912
|
+
});
|
|
2913
|
+
return ok({ applied: true });
|
|
2914
|
+
});
|
|
2915
|
+
}
|
|
2916
|
+
};
|
|
2917
|
+
|
|
1813
2918
|
// src/tools/atomic/browser_snapshot.ts
|
|
1814
2919
|
var browserSnapshotTool = {
|
|
1815
2920
|
name: ToolNames.browserSnapshot,
|
|
@@ -1832,6 +2937,26 @@ var browserSnapshotTool = {
|
|
|
1832
2937
|
}
|
|
1833
2938
|
};
|
|
1834
2939
|
|
|
2940
|
+
// src/tools/atomic/browser_switch_page.ts
|
|
2941
|
+
var browserSwitchPageTool = {
|
|
2942
|
+
name: ToolNames.browserSwitchPage,
|
|
2943
|
+
description: "Set the active page for subsequent tool calls. Use `browser_pages` to discover indexes. Switching invalidates all refs because each page has its own DOM. Page 0 is the main page; popups land at higher indexes in the order they opened.",
|
|
2944
|
+
inputShape: browserSwitchPageShape,
|
|
2945
|
+
build(ctx) {
|
|
2946
|
+
return safeHandler(async (args) => {
|
|
2947
|
+
const engine = ctx.registry.engineFor(args.session_id);
|
|
2948
|
+
if (!(engine instanceof PlaywrightEngine)) {
|
|
2949
|
+
throw new RolepodMcpError(
|
|
2950
|
+
"unsupported_engine",
|
|
2951
|
+
"switch_page is web-only and requires PlaywrightEngine."
|
|
2952
|
+
);
|
|
2953
|
+
}
|
|
2954
|
+
await engine.switchPage(args.session_id, args.index);
|
|
2955
|
+
return ok({ active_index: args.index });
|
|
2956
|
+
});
|
|
2957
|
+
}
|
|
2958
|
+
};
|
|
2959
|
+
|
|
1835
2960
|
// src/tools/atomic/browser_type.ts
|
|
1836
2961
|
var browserTypeTool = {
|
|
1837
2962
|
name: ToolNames.browserType,
|
|
@@ -1851,6 +2976,27 @@ var browserTypeTool = {
|
|
|
1851
2976
|
}
|
|
1852
2977
|
};
|
|
1853
2978
|
|
|
2979
|
+
// src/tools/atomic/browser_upload_file.ts
|
|
2980
|
+
var browserUploadFileTool = {
|
|
2981
|
+
name: ToolNames.browserUploadFile,
|
|
2982
|
+
description: "Attach a local file to the `<input type=file>` element identified by `ref`. `file_path` MUST be an absolute path on the host filesystem. Invalidates all refs on success.",
|
|
2983
|
+
inputShape: browserUploadFileShape,
|
|
2984
|
+
build(ctx) {
|
|
2985
|
+
return safeHandler(async (args) => {
|
|
2986
|
+
const engine = ctx.registry.engineFor(args.session_id);
|
|
2987
|
+
await engine.uploadFile(
|
|
2988
|
+
{
|
|
2989
|
+
id: args.session_id,
|
|
2990
|
+
platform: ctx.registry.platformOf(args.session_id)
|
|
2991
|
+
},
|
|
2992
|
+
args.ref,
|
|
2993
|
+
args.file_path
|
|
2994
|
+
);
|
|
2995
|
+
return ok({ uploaded: true, file_path: args.file_path });
|
|
2996
|
+
});
|
|
2997
|
+
}
|
|
2998
|
+
};
|
|
2999
|
+
|
|
1854
3000
|
// src/tools/atomic/browser_wait_for.ts
|
|
1855
3001
|
var browserWaitForTool = {
|
|
1856
3002
|
name: ToolNames.browserWaitFor,
|
|
@@ -2223,6 +3369,67 @@ function playwrightStepLine(step) {
|
|
|
2223
3369
|
return ` await page.goto(${JSON.stringify(step.url)});`;
|
|
2224
3370
|
case "wait_for":
|
|
2225
3371
|
return ` // wait_for: ${JSON.stringify(step.condition)} \u2014 translate to page.waitForXxx()`;
|
|
3372
|
+
case "hover":
|
|
3373
|
+
return ` await page.getByText(${JSON.stringify(step.query)}, { exact: false }).first().hover();`;
|
|
3374
|
+
case "drag":
|
|
3375
|
+
return [
|
|
3376
|
+
` await page`,
|
|
3377
|
+
` .getByText(${JSON.stringify(step.from_query)}, { exact: false })`,
|
|
3378
|
+
` .first()`,
|
|
3379
|
+
` .dragTo(page.getByText(${JSON.stringify(step.to_query)}, { exact: false }).first());`
|
|
3380
|
+
].join("\n");
|
|
3381
|
+
case "fill_form": {
|
|
3382
|
+
const fields = Array.isArray(step.fields) ? step.fields : [];
|
|
3383
|
+
return fields.map((f) => {
|
|
3384
|
+
const q = JSON.stringify(f.query);
|
|
3385
|
+
if (f.kind === "select") {
|
|
3386
|
+
return ` await page.getByLabel(${q}).selectOption(${JSON.stringify(String(f.value))});`;
|
|
3387
|
+
}
|
|
3388
|
+
if (f.kind === "checkbox" || f.kind === "radio") {
|
|
3389
|
+
const checked = typeof f.value === "boolean" ? f.value : String(f.value) === "true" || String(f.value) === "on";
|
|
3390
|
+
return ` await page.getByLabel(${q}).setChecked(${checked});`;
|
|
3391
|
+
}
|
|
3392
|
+
return ` await page.getByLabel(${q}).fill(${JSON.stringify(String(f.value))});`;
|
|
3393
|
+
}).join("\n");
|
|
3394
|
+
}
|
|
3395
|
+
case "upload":
|
|
3396
|
+
return ` await page.getByLabel(${JSON.stringify(step.query)}).setInputFiles(${JSON.stringify(step.file_path)});`;
|
|
3397
|
+
case "dialog":
|
|
3398
|
+
return [
|
|
3399
|
+
` page.once("dialog", async (dialog) => {`,
|
|
3400
|
+
step.action === "accept" ? ` await dialog.accept();` : step.action === "accept_with_text" ? ` await dialog.accept(${JSON.stringify(step.text ?? "")});` : ` await dialog.dismiss();`,
|
|
3401
|
+
` });`
|
|
3402
|
+
].join("\n");
|
|
3403
|
+
case "set_env": {
|
|
3404
|
+
const lines = [];
|
|
3405
|
+
if (step.viewport && typeof step.viewport === "object") {
|
|
3406
|
+
const v = step.viewport;
|
|
3407
|
+
lines.push(` await page.setViewportSize({ width: ${v.width}, height: ${v.height} });`);
|
|
3408
|
+
}
|
|
3409
|
+
if (step.offline !== void 0) {
|
|
3410
|
+
lines.push(` await page.context().setOffline(${Boolean(step.offline)});`);
|
|
3411
|
+
}
|
|
3412
|
+
if (step.geolocation) {
|
|
3413
|
+
lines.push(` await page.context().setGeolocation(${JSON.stringify(step.geolocation)});`);
|
|
3414
|
+
}
|
|
3415
|
+
if (step.color_scheme || step.reduced_motion) {
|
|
3416
|
+
const opts = {};
|
|
3417
|
+
if (step.color_scheme) opts.colorScheme = step.color_scheme;
|
|
3418
|
+
if (step.reduced_motion) opts.reducedMotion = step.reduced_motion;
|
|
3419
|
+
lines.push(` await page.emulateMedia(${JSON.stringify(opts)});`);
|
|
3420
|
+
}
|
|
3421
|
+
if (step.extra_headers) {
|
|
3422
|
+
lines.push(` await page.context().setExtraHTTPHeaders(${JSON.stringify(step.extra_headers)});`);
|
|
3423
|
+
}
|
|
3424
|
+
if (step.network_throttle || step.cpu_throttle !== void 0) {
|
|
3425
|
+
lines.push(` // network/cpu throttle requires CDP \u2014 see Playwright docs (chromium only)`);
|
|
3426
|
+
}
|
|
3427
|
+
return lines.length > 0 ? lines.join("\n") : ` // set_env: nothing to apply`;
|
|
3428
|
+
}
|
|
3429
|
+
case "switch_page":
|
|
3430
|
+
return ` const allPages = page.context().pages(); /* switch to index ${step.index} */ if (allPages[${step.index}]) await allPages[${step.index}].bringToFront();`;
|
|
3431
|
+
case "evaluate":
|
|
3432
|
+
return ` await page.evaluate(${JSON.stringify(step.script)});`;
|
|
2226
3433
|
default:
|
|
2227
3434
|
return ` // unsupported step kind: ${step.kind}`;
|
|
2228
3435
|
}
|
|
@@ -2237,6 +3444,20 @@ function playwrightExpectLine(exp) {
|
|
|
2237
3444
|
return ` await expect(page).toHaveURL(new RegExp(${JSON.stringify(exp.pattern)}));`;
|
|
2238
3445
|
case "ref_in_state":
|
|
2239
3446
|
return ` // ref_in_state ${JSON.stringify(exp.query)} \u2192 ${String(exp.state)} \u2014 translate as needed`;
|
|
3447
|
+
case "no_console_errors":
|
|
3448
|
+
return [
|
|
3449
|
+
` // no_console_errors \u2014 collect via page.on('console') before the steps, then:`,
|
|
3450
|
+
` // expect(consoleErrors).toEqual([]);`
|
|
3451
|
+
].join("\n");
|
|
3452
|
+
case "no_failed_requests":
|
|
3453
|
+
return [
|
|
3454
|
+
` // no_failed_requests \u2014 collect via page.on('requestfailed'/'response') before the steps, then:`,
|
|
3455
|
+
` // expect(failedRequests).toEqual([]);`
|
|
3456
|
+
].join("\n");
|
|
3457
|
+
case "request_made":
|
|
3458
|
+
return ` await page.waitForRequest(new RegExp(${JSON.stringify(exp.url_pattern)}));`;
|
|
3459
|
+
case "response_status":
|
|
3460
|
+
return ` await page.waitForResponse((r) => new RegExp(${JSON.stringify(exp.url_pattern)}).test(r.url()) && r.status() === ${Number(exp.status)});`;
|
|
2240
3461
|
default:
|
|
2241
3462
|
return ` // unsupported expect kind: ${exp.kind}`;
|
|
2242
3463
|
}
|
|
@@ -2253,6 +3474,56 @@ function seleniumStepLine(step) {
|
|
|
2253
3474
|
return ` driver.get(${JSON.stringify(step.url)})`;
|
|
2254
3475
|
case "wait_for":
|
|
2255
3476
|
return ` # wait_for: ${JSON.stringify(step.condition)} \u2014 translate to WebDriverWait`;
|
|
3477
|
+
case "hover":
|
|
3478
|
+
return [
|
|
3479
|
+
` from selenium.webdriver.common.action_chains import ActionChains`,
|
|
3480
|
+
` target = driver.find_element(By.XPATH, f"//*[contains(text(), \\"${escapePy(String(step.query))}\\")]")`,
|
|
3481
|
+
` ActionChains(driver).move_to_element(target).perform()`
|
|
3482
|
+
].join("\n");
|
|
3483
|
+
case "drag":
|
|
3484
|
+
return [
|
|
3485
|
+
` from selenium.webdriver.common.action_chains import ActionChains`,
|
|
3486
|
+
` src = driver.find_element(By.XPATH, f"//*[contains(text(), \\"${escapePy(String(step.from_query))}\\")]")`,
|
|
3487
|
+
` dst = driver.find_element(By.XPATH, f"//*[contains(text(), \\"${escapePy(String(step.to_query))}\\")]")`,
|
|
3488
|
+
` ActionChains(driver).drag_and_drop(src, dst).perform()`
|
|
3489
|
+
].join("\n");
|
|
3490
|
+
case "fill_form": {
|
|
3491
|
+
const fields = Array.isArray(step.fields) ? step.fields : [];
|
|
3492
|
+
return fields.map((f) => {
|
|
3493
|
+
const q = escapePy(f.query);
|
|
3494
|
+
if (f.kind === "select") {
|
|
3495
|
+
return [
|
|
3496
|
+
` from selenium.webdriver.support.ui import Select`,
|
|
3497
|
+
` Select(driver.find_element(By.XPATH, f"//*[@aria-label=\\"${q}\\"]")).select_by_visible_text(${JSON.stringify(String(f.value))})`
|
|
3498
|
+
].join("\n");
|
|
3499
|
+
}
|
|
3500
|
+
if (f.kind === "checkbox" || f.kind === "radio") {
|
|
3501
|
+
const checked = typeof f.value === "boolean" ? f.value : String(f.value) === "true";
|
|
3502
|
+
return ` el = driver.find_element(By.XPATH, f"//*[@aria-label=\\"${q}\\"]"); el.click() if el.is_selected() != ${checked ? "True" : "False"} else None`;
|
|
3503
|
+
}
|
|
3504
|
+
return ` driver.find_element(By.XPATH, f"//*[@aria-label=\\"${q}\\"]").send_keys(${JSON.stringify(String(f.value))})`;
|
|
3505
|
+
}).join("\n");
|
|
3506
|
+
}
|
|
3507
|
+
case "upload":
|
|
3508
|
+
return ` driver.find_element(By.XPATH, f"//*[@aria-label=\\"${escapePy(String(step.query))}\\"]").send_keys(${JSON.stringify(step.file_path)})`;
|
|
3509
|
+
case "dialog":
|
|
3510
|
+
return [
|
|
3511
|
+
` alert = driver.switch_to.alert`,
|
|
3512
|
+
step.action === "accept" ? ` alert.accept()` : step.action === "accept_with_text" ? ` alert.send_keys(${JSON.stringify(step.text ?? "")}); alert.accept()` : ` alert.dismiss()`
|
|
3513
|
+
].join("\n");
|
|
3514
|
+
case "set_env": {
|
|
3515
|
+
const lines = [];
|
|
3516
|
+
if (step.viewport && typeof step.viewport === "object") {
|
|
3517
|
+
const v = step.viewport;
|
|
3518
|
+
lines.push(` driver.set_window_size(${v.width}, ${v.height})`);
|
|
3519
|
+
}
|
|
3520
|
+
lines.push(` # set_env partially supported in Selenium \u2014 see selenium docs for offline/geolocation/colorScheme via CDP`);
|
|
3521
|
+
return lines.join("\n");
|
|
3522
|
+
}
|
|
3523
|
+
case "switch_page":
|
|
3524
|
+
return ` driver.switch_to.window(driver.window_handles[${step.index}])`;
|
|
3525
|
+
case "evaluate":
|
|
3526
|
+
return ` driver.execute_script(${JSON.stringify(step.script)})`;
|
|
2256
3527
|
default:
|
|
2257
3528
|
return ` # unsupported step kind: ${step.kind}`;
|
|
2258
3529
|
}
|
|
@@ -2267,6 +3538,18 @@ function seleniumExpectLine(exp) {
|
|
|
2267
3538
|
return ` import re; assert re.search(${JSON.stringify(exp.pattern)}, driver.current_url)`;
|
|
2268
3539
|
case "ref_in_state":
|
|
2269
3540
|
return ` # ref_in_state ${JSON.stringify(exp.query)} \u2192 ${String(exp.state)}`;
|
|
3541
|
+
case "no_console_errors":
|
|
3542
|
+
return [
|
|
3543
|
+
` # no_console_errors \u2014 read browser logs via driver.get_log("browser")`,
|
|
3544
|
+
` errors = [l for l in driver.get_log("browser") if l.get("level") == "SEVERE"]`,
|
|
3545
|
+
` assert errors == [], f"console errors: {errors}"`
|
|
3546
|
+
].join("\n");
|
|
3547
|
+
case "no_failed_requests":
|
|
3548
|
+
return ` # no_failed_requests \u2014 selenium has no built-in network capture. Enable selenium-wire or BiDi for this.`;
|
|
3549
|
+
case "request_made":
|
|
3550
|
+
return ` # request_made ${JSON.stringify(exp.url_pattern)} \u2014 use selenium-wire (driver.requests) or BiDi`;
|
|
3551
|
+
case "response_status":
|
|
3552
|
+
return ` # response_status ${JSON.stringify(exp.url_pattern)} == ${Number(exp.status)} \u2014 use selenium-wire (driver.requests) or BiDi`;
|
|
2270
3553
|
default:
|
|
2271
3554
|
return ` # unsupported expect kind: ${exp.kind}`;
|
|
2272
3555
|
}
|
|
@@ -2398,9 +3681,265 @@ var visualDiffTool = {
|
|
|
2398
3681
|
}
|
|
2399
3682
|
};
|
|
2400
3683
|
|
|
3684
|
+
// src/tools/metadata.ts
|
|
3685
|
+
var toolMetadata = {
|
|
3686
|
+
// ---------- atomic ----------
|
|
3687
|
+
[ToolNames.browserOpen]: {
|
|
3688
|
+
title: "Open Browser/Mobile Session",
|
|
3689
|
+
annotations: {
|
|
3690
|
+
title: "Open Browser/Mobile Session",
|
|
3691
|
+
readOnlyHint: false,
|
|
3692
|
+
destructiveHint: true,
|
|
3693
|
+
idempotentHint: false,
|
|
3694
|
+
openWorldHint: true
|
|
3695
|
+
}
|
|
3696
|
+
},
|
|
3697
|
+
[ToolNames.browserClose]: {
|
|
3698
|
+
title: "Close Session",
|
|
3699
|
+
annotations: {
|
|
3700
|
+
title: "Close Session",
|
|
3701
|
+
readOnlyHint: false,
|
|
3702
|
+
destructiveHint: true,
|
|
3703
|
+
idempotentHint: true,
|
|
3704
|
+
openWorldHint: false
|
|
3705
|
+
}
|
|
3706
|
+
},
|
|
3707
|
+
[ToolNames.browserSnapshot]: {
|
|
3708
|
+
title: "Capture Accessibility Snapshot",
|
|
3709
|
+
annotations: {
|
|
3710
|
+
title: "Capture Accessibility Snapshot",
|
|
3711
|
+
readOnlyHint: true,
|
|
3712
|
+
openWorldHint: true
|
|
3713
|
+
}
|
|
3714
|
+
},
|
|
3715
|
+
[ToolNames.browserClick]: {
|
|
3716
|
+
title: "Click Element",
|
|
3717
|
+
annotations: {
|
|
3718
|
+
title: "Click Element",
|
|
3719
|
+
readOnlyHint: false,
|
|
3720
|
+
destructiveHint: true,
|
|
3721
|
+
idempotentHint: false,
|
|
3722
|
+
openWorldHint: true
|
|
3723
|
+
}
|
|
3724
|
+
},
|
|
3725
|
+
[ToolNames.browserType]: {
|
|
3726
|
+
title: "Type Text",
|
|
3727
|
+
annotations: {
|
|
3728
|
+
title: "Type Text",
|
|
3729
|
+
readOnlyHint: false,
|
|
3730
|
+
destructiveHint: true,
|
|
3731
|
+
idempotentHint: false,
|
|
3732
|
+
openWorldHint: true
|
|
3733
|
+
}
|
|
3734
|
+
},
|
|
3735
|
+
[ToolNames.browserKey]: {
|
|
3736
|
+
title: "Press Key",
|
|
3737
|
+
annotations: {
|
|
3738
|
+
title: "Press Key",
|
|
3739
|
+
readOnlyHint: false,
|
|
3740
|
+
destructiveHint: true,
|
|
3741
|
+
idempotentHint: false,
|
|
3742
|
+
openWorldHint: true
|
|
3743
|
+
}
|
|
3744
|
+
},
|
|
3745
|
+
[ToolNames.browserScroll]: {
|
|
3746
|
+
title: "Scroll Viewport",
|
|
3747
|
+
annotations: {
|
|
3748
|
+
title: "Scroll Viewport",
|
|
3749
|
+
readOnlyHint: false,
|
|
3750
|
+
destructiveHint: false,
|
|
3751
|
+
idempotentHint: true,
|
|
3752
|
+
openWorldHint: true
|
|
3753
|
+
}
|
|
3754
|
+
},
|
|
3755
|
+
[ToolNames.browserWaitFor]: {
|
|
3756
|
+
title: "Wait For Condition",
|
|
3757
|
+
annotations: {
|
|
3758
|
+
title: "Wait For Condition",
|
|
3759
|
+
readOnlyHint: true,
|
|
3760
|
+
openWorldHint: true
|
|
3761
|
+
}
|
|
3762
|
+
},
|
|
3763
|
+
[ToolNames.browserScreenshot]: {
|
|
3764
|
+
title: "Take Screenshot",
|
|
3765
|
+
annotations: {
|
|
3766
|
+
title: "Take Screenshot",
|
|
3767
|
+
readOnlyHint: true,
|
|
3768
|
+
openWorldHint: true
|
|
3769
|
+
}
|
|
3770
|
+
},
|
|
3771
|
+
[ToolNames.browserNavigate]: {
|
|
3772
|
+
title: "Navigate URL",
|
|
3773
|
+
annotations: {
|
|
3774
|
+
title: "Navigate URL",
|
|
3775
|
+
readOnlyHint: false,
|
|
3776
|
+
destructiveHint: true,
|
|
3777
|
+
idempotentHint: false,
|
|
3778
|
+
openWorldHint: true
|
|
3779
|
+
}
|
|
3780
|
+
},
|
|
3781
|
+
// ---------- composite ----------
|
|
3782
|
+
[ToolNames.verifyUiFlow]: {
|
|
3783
|
+
title: "Verify UI Flow",
|
|
3784
|
+
annotations: {
|
|
3785
|
+
title: "Verify UI Flow",
|
|
3786
|
+
readOnlyHint: false,
|
|
3787
|
+
destructiveHint: true,
|
|
3788
|
+
idempotentHint: false,
|
|
3789
|
+
openWorldHint: true
|
|
3790
|
+
}
|
|
3791
|
+
},
|
|
3792
|
+
[ToolNames.auditA11y]: {
|
|
3793
|
+
title: "Audit Accessibility (axe-core)",
|
|
3794
|
+
annotations: {
|
|
3795
|
+
title: "Audit Accessibility (axe-core)",
|
|
3796
|
+
readOnlyHint: true,
|
|
3797
|
+
openWorldHint: true
|
|
3798
|
+
}
|
|
3799
|
+
},
|
|
3800
|
+
[ToolNames.visualDiff]: {
|
|
3801
|
+
title: "Visual Diff vs Baseline",
|
|
3802
|
+
annotations: {
|
|
3803
|
+
title: "Visual Diff vs Baseline",
|
|
3804
|
+
// Writes to ./.rolepod-uiproof/{baselines,artifacts}/ but only adds files —
|
|
3805
|
+
// never destroys an existing baseline silently.
|
|
3806
|
+
readOnlyHint: false,
|
|
3807
|
+
destructiveHint: false,
|
|
3808
|
+
idempotentHint: true,
|
|
3809
|
+
openWorldHint: true
|
|
3810
|
+
}
|
|
3811
|
+
},
|
|
3812
|
+
[ToolNames.scaffoldE2e]: {
|
|
3813
|
+
title: "Scaffold E2E Test File",
|
|
3814
|
+
annotations: {
|
|
3815
|
+
title: "Scaffold E2E Test File",
|
|
3816
|
+
// Writes a test file to the local repo.
|
|
3817
|
+
readOnlyHint: false,
|
|
3818
|
+
destructiveHint: true,
|
|
3819
|
+
idempotentHint: false,
|
|
3820
|
+
openWorldHint: false
|
|
3821
|
+
}
|
|
3822
|
+
},
|
|
3823
|
+
[ToolNames.extractUiState]: {
|
|
3824
|
+
title: "Extract UI State (NL Query)",
|
|
3825
|
+
annotations: {
|
|
3826
|
+
title: "Extract UI State (NL Query)",
|
|
3827
|
+
readOnlyHint: true,
|
|
3828
|
+
openWorldHint: true
|
|
3829
|
+
}
|
|
3830
|
+
},
|
|
3831
|
+
// ---------- v0.5 atomic additions ----------
|
|
3832
|
+
[ToolNames.browserHover]: {
|
|
3833
|
+
title: "Hover Element",
|
|
3834
|
+
annotations: {
|
|
3835
|
+
title: "Hover Element",
|
|
3836
|
+
readOnlyHint: false,
|
|
3837
|
+
destructiveHint: false,
|
|
3838
|
+
idempotentHint: true,
|
|
3839
|
+
openWorldHint: true
|
|
3840
|
+
}
|
|
3841
|
+
},
|
|
3842
|
+
[ToolNames.browserDrag]: {
|
|
3843
|
+
title: "Drag Element",
|
|
3844
|
+
annotations: {
|
|
3845
|
+
title: "Drag Element",
|
|
3846
|
+
readOnlyHint: false,
|
|
3847
|
+
destructiveHint: true,
|
|
3848
|
+
idempotentHint: false,
|
|
3849
|
+
openWorldHint: true
|
|
3850
|
+
}
|
|
3851
|
+
},
|
|
3852
|
+
[ToolNames.browserFillForm]: {
|
|
3853
|
+
title: "Fill Form (Batch)",
|
|
3854
|
+
annotations: {
|
|
3855
|
+
title: "Fill Form (Batch)",
|
|
3856
|
+
readOnlyHint: false,
|
|
3857
|
+
destructiveHint: true,
|
|
3858
|
+
idempotentHint: false,
|
|
3859
|
+
openWorldHint: true
|
|
3860
|
+
}
|
|
3861
|
+
},
|
|
3862
|
+
[ToolNames.browserUploadFile]: {
|
|
3863
|
+
title: "Upload File",
|
|
3864
|
+
annotations: {
|
|
3865
|
+
title: "Upload File",
|
|
3866
|
+
readOnlyHint: false,
|
|
3867
|
+
destructiveHint: true,
|
|
3868
|
+
idempotentHint: false,
|
|
3869
|
+
openWorldHint: true
|
|
3870
|
+
}
|
|
3871
|
+
},
|
|
3872
|
+
[ToolNames.browserHandleDialog]: {
|
|
3873
|
+
title: "Pre-arm Dialog Handler",
|
|
3874
|
+
annotations: {
|
|
3875
|
+
title: "Pre-arm Dialog Handler",
|
|
3876
|
+
readOnlyHint: false,
|
|
3877
|
+
destructiveHint: true,
|
|
3878
|
+
idempotentHint: false,
|
|
3879
|
+
openWorldHint: false
|
|
3880
|
+
}
|
|
3881
|
+
},
|
|
3882
|
+
[ToolNames.browserConsole]: {
|
|
3883
|
+
title: "Inspect Console Logs",
|
|
3884
|
+
annotations: {
|
|
3885
|
+
title: "Inspect Console Logs",
|
|
3886
|
+
readOnlyHint: true,
|
|
3887
|
+
openWorldHint: false
|
|
3888
|
+
}
|
|
3889
|
+
},
|
|
3890
|
+
[ToolNames.browserNetwork]: {
|
|
3891
|
+
title: "Inspect Network Requests",
|
|
3892
|
+
annotations: {
|
|
3893
|
+
title: "Inspect Network Requests",
|
|
3894
|
+
readOnlyHint: true,
|
|
3895
|
+
openWorldHint: false
|
|
3896
|
+
}
|
|
3897
|
+
},
|
|
3898
|
+
[ToolNames.browserSetEnv]: {
|
|
3899
|
+
title: "Set Browser Environment",
|
|
3900
|
+
annotations: {
|
|
3901
|
+
title: "Set Browser Environment",
|
|
3902
|
+
readOnlyHint: false,
|
|
3903
|
+
destructiveHint: true,
|
|
3904
|
+
idempotentHint: true,
|
|
3905
|
+
openWorldHint: false
|
|
3906
|
+
}
|
|
3907
|
+
},
|
|
3908
|
+
[ToolNames.browserEvaluate]: {
|
|
3909
|
+
title: "Evaluate JavaScript (gated; arbitrary code execution)",
|
|
3910
|
+
annotations: {
|
|
3911
|
+
title: "Evaluate JavaScript",
|
|
3912
|
+
// Arbitrary code execution in the page context. Gated by
|
|
3913
|
+
// ROLEPOD_ALLOW_EVAL=1 server-side. Always treat as destructive.
|
|
3914
|
+
readOnlyHint: false,
|
|
3915
|
+
destructiveHint: true,
|
|
3916
|
+
idempotentHint: false,
|
|
3917
|
+
openWorldHint: true
|
|
3918
|
+
}
|
|
3919
|
+
},
|
|
3920
|
+
[ToolNames.browserPages]: {
|
|
3921
|
+
title: "List Open Pages",
|
|
3922
|
+
annotations: {
|
|
3923
|
+
title: "List Open Pages",
|
|
3924
|
+
readOnlyHint: true,
|
|
3925
|
+
openWorldHint: false
|
|
3926
|
+
}
|
|
3927
|
+
},
|
|
3928
|
+
[ToolNames.browserSwitchPage]: {
|
|
3929
|
+
title: "Switch Active Page",
|
|
3930
|
+
annotations: {
|
|
3931
|
+
title: "Switch Active Page",
|
|
3932
|
+
readOnlyHint: false,
|
|
3933
|
+
destructiveHint: false,
|
|
3934
|
+
idempotentHint: true,
|
|
3935
|
+
openWorldHint: false
|
|
3936
|
+
}
|
|
3937
|
+
}
|
|
3938
|
+
};
|
|
3939
|
+
|
|
2401
3940
|
// src/server.ts
|
|
2402
3941
|
var SERVER_NAME = "rolepod-uiproof";
|
|
2403
|
-
var SERVER_VERSION = "0.
|
|
3942
|
+
var SERVER_VERSION = "0.5.0";
|
|
2404
3943
|
function buildServer(opts = {}) {
|
|
2405
3944
|
const webEngine = createWebEngine();
|
|
2406
3945
|
const registry = new SessionRegistry({ idleTimeoutMs: opts.idleTimeoutMs });
|
|
@@ -2417,7 +3956,7 @@ function buildServer(opts = {}) {
|
|
|
2417
3956
|
version: SERVER_VERSION
|
|
2418
3957
|
});
|
|
2419
3958
|
const tools = [
|
|
2420
|
-
// atomic
|
|
3959
|
+
// atomic (v0.1-v0.4)
|
|
2421
3960
|
browserOpenTool,
|
|
2422
3961
|
browserCloseTool,
|
|
2423
3962
|
browserSnapshotTool,
|
|
@@ -2428,6 +3967,18 @@ function buildServer(opts = {}) {
|
|
|
2428
3967
|
browserWaitForTool,
|
|
2429
3968
|
browserScreenshotTool,
|
|
2430
3969
|
browserNavigateTool,
|
|
3970
|
+
// atomic (v0.5)
|
|
3971
|
+
browserHoverTool,
|
|
3972
|
+
browserDragTool,
|
|
3973
|
+
browserFillFormTool,
|
|
3974
|
+
browserUploadFileTool,
|
|
3975
|
+
browserHandleDialogTool,
|
|
3976
|
+
browserConsoleTool,
|
|
3977
|
+
browserNetworkTool,
|
|
3978
|
+
browserSetEnvTool,
|
|
3979
|
+
browserEvaluateTool,
|
|
3980
|
+
browserPagesTool,
|
|
3981
|
+
browserSwitchPageTool,
|
|
2431
3982
|
// composite
|
|
2432
3983
|
verifyUiFlowTool,
|
|
2433
3984
|
auditA11yTool,
|
|
@@ -2436,9 +3987,15 @@ function buildServer(opts = {}) {
|
|
|
2436
3987
|
extractUiStateTool
|
|
2437
3988
|
];
|
|
2438
3989
|
for (const t of tools) {
|
|
3990
|
+
const meta = toolMetadata[t.name];
|
|
2439
3991
|
mcp.registerTool(
|
|
2440
3992
|
t.name,
|
|
2441
|
-
{
|
|
3993
|
+
{
|
|
3994
|
+
title: meta?.title,
|
|
3995
|
+
description: t.description,
|
|
3996
|
+
inputSchema: t.inputShape,
|
|
3997
|
+
annotations: meta?.annotations
|
|
3998
|
+
},
|
|
2442
3999
|
t.build(ctx)
|
|
2443
4000
|
);
|
|
2444
4001
|
}
|