@rolepod/uiproof 0.4.1 → 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 +2 -2
- package/.codex-plugin/plugin.json +3 -3
- package/.cursor-plugin/plugin.json +2 -2
- package/CHANGELOG.md +99 -0
- package/README.md +11 -7
- package/dist/bin/rolepod-uiproof.js +1434 -32
- package/dist/bin/rolepod-uiproof.js.map +1 -1
- package/dist/index.d.ts +590 -2
- package/dist/index.js +1456 -32
- 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 +137 -70
package/dist/index.js
CHANGED
|
@@ -415,6 +415,34 @@ var AppiumEngine = class {
|
|
|
415
415
|
throw new UnsupportedPlatformError(_session.platform);
|
|
416
416
|
}
|
|
417
417
|
// -------------------------------------------------------------------------
|
|
418
|
+
// v0.5 cross-platform additions — mobile stubs.
|
|
419
|
+
// These ship as `not_implemented_in_v05` until the mobile gesture work lands.
|
|
420
|
+
// -------------------------------------------------------------------------
|
|
421
|
+
async hover(_session, _ref) {
|
|
422
|
+
throw new RolepodMcpError(
|
|
423
|
+
"engine_error",
|
|
424
|
+
"hover is not yet implemented for mobile (Appium). Use long-press via custom gesture if needed."
|
|
425
|
+
);
|
|
426
|
+
}
|
|
427
|
+
async drag(_session, _fromRef, _toRef) {
|
|
428
|
+
throw new RolepodMcpError(
|
|
429
|
+
"engine_error",
|
|
430
|
+
"drag is not yet implemented for mobile (Appium). Use the W3C Actions API directly if needed."
|
|
431
|
+
);
|
|
432
|
+
}
|
|
433
|
+
async fillForm(session, fields) {
|
|
434
|
+
for (const f of fields) {
|
|
435
|
+
const v = typeof f.value === "boolean" ? String(f.value) : f.value;
|
|
436
|
+
await this.type(session, f.ref, v);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
async uploadFile(_session, _ref, _filePath) {
|
|
440
|
+
throw new RolepodMcpError(
|
|
441
|
+
"engine_error",
|
|
442
|
+
"upload_file is not supported on mobile (Appium)."
|
|
443
|
+
);
|
|
444
|
+
}
|
|
445
|
+
// -------------------------------------------------------------------------
|
|
418
446
|
// Internals
|
|
419
447
|
// -------------------------------------------------------------------------
|
|
420
448
|
async loadWdio() {
|
|
@@ -529,6 +557,7 @@ function treeIncludesText(node, text) {
|
|
|
529
557
|
|
|
530
558
|
// src/engine/PlaywrightEngine.ts
|
|
531
559
|
import { randomUUID as randomUUID3 } from "crypto";
|
|
560
|
+
import { resolve as resolvePath, isAbsolute } from "path";
|
|
532
561
|
import {
|
|
533
562
|
chromium,
|
|
534
563
|
firefox,
|
|
@@ -638,6 +667,83 @@ function parseAriaSnapshot(snapshotYaml) {
|
|
|
638
667
|
}
|
|
639
668
|
|
|
640
669
|
// src/engine/PlaywrightEngine.ts
|
|
670
|
+
var CONSOLE_BUFFER_CAP = 1e3;
|
|
671
|
+
var NETWORK_BUFFER_CAP = 1e3;
|
|
672
|
+
var NETWORK_PRESETS = {
|
|
673
|
+
offline: { offline: true, downloadThroughput: 0, uploadThroughput: 0, latency: 0 },
|
|
674
|
+
"slow-3g": {
|
|
675
|
+
offline: false,
|
|
676
|
+
// 500 Kbps down / 500 Kbps up / 400ms RTT
|
|
677
|
+
downloadThroughput: 500 * 1024 / 8,
|
|
678
|
+
uploadThroughput: 500 * 1024 / 8,
|
|
679
|
+
latency: 400
|
|
680
|
+
},
|
|
681
|
+
"fast-3g": {
|
|
682
|
+
offline: false,
|
|
683
|
+
downloadThroughput: 1.5 * 1024 * 1024 / 8,
|
|
684
|
+
uploadThroughput: 750 * 1024 / 8,
|
|
685
|
+
latency: 150
|
|
686
|
+
},
|
|
687
|
+
"slow-4g": {
|
|
688
|
+
offline: false,
|
|
689
|
+
downloadThroughput: 4 * 1024 * 1024 / 8,
|
|
690
|
+
uploadThroughput: 3 * 1024 * 1024 / 8,
|
|
691
|
+
latency: 100
|
|
692
|
+
},
|
|
693
|
+
"fast-4g": {
|
|
694
|
+
offline: false,
|
|
695
|
+
downloadThroughput: 9 * 1024 * 1024 / 8,
|
|
696
|
+
uploadThroughput: 4.5 * 1024 * 1024 / 8,
|
|
697
|
+
latency: 60
|
|
698
|
+
},
|
|
699
|
+
"no-throttling": {
|
|
700
|
+
offline: false,
|
|
701
|
+
downloadThroughput: -1,
|
|
702
|
+
uploadThroughput: -1,
|
|
703
|
+
latency: 0
|
|
704
|
+
}
|
|
705
|
+
};
|
|
706
|
+
function pushRing(buf, entry, cap) {
|
|
707
|
+
buf.push(entry);
|
|
708
|
+
if (buf.length > cap) buf.splice(0, buf.length - cap);
|
|
709
|
+
}
|
|
710
|
+
function mapConsoleLevel(t) {
|
|
711
|
+
switch (t) {
|
|
712
|
+
case "error":
|
|
713
|
+
return "error";
|
|
714
|
+
case "warning":
|
|
715
|
+
return "warning";
|
|
716
|
+
case "info":
|
|
717
|
+
return "info";
|
|
718
|
+
case "debug":
|
|
719
|
+
return "debug";
|
|
720
|
+
case "trace":
|
|
721
|
+
return "trace";
|
|
722
|
+
default:
|
|
723
|
+
return "log";
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
function formatConsoleLocation(msg) {
|
|
727
|
+
try {
|
|
728
|
+
const loc = msg.location();
|
|
729
|
+
if (!loc?.url) return void 0;
|
|
730
|
+
return `${loc.url}:${loc.lineNumber}:${loc.columnNumber}`;
|
|
731
|
+
} catch {
|
|
732
|
+
return void 0;
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
function findNetworkEntry(buf, req) {
|
|
736
|
+
const url = req.url();
|
|
737
|
+
const method = req.method();
|
|
738
|
+
for (let i = buf.length - 1; i >= 0; i--) {
|
|
739
|
+
const e = buf[i];
|
|
740
|
+
if (!e) continue;
|
|
741
|
+
if (e.url === url && e.method === method && e.status === void 0 && !e.failure) {
|
|
742
|
+
return e;
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
return void 0;
|
|
746
|
+
}
|
|
641
747
|
var PlaywrightEngine = class {
|
|
642
748
|
id = "playwright";
|
|
643
749
|
sessions = /* @__PURE__ */ new Map();
|
|
@@ -653,35 +759,80 @@ var PlaywrightEngine = class {
|
|
|
653
759
|
if (opts.viewport) contextOptions.viewport = opts.viewport;
|
|
654
760
|
if (opts.user_agent) contextOptions.userAgent = opts.user_agent;
|
|
655
761
|
if (opts.locale) contextOptions.locale = opts.locale;
|
|
762
|
+
if (opts.capture?.har) {
|
|
763
|
+
contextOptions.recordHar = { path: opts.capture.har.path };
|
|
764
|
+
}
|
|
765
|
+
if (opts.capture?.video) {
|
|
766
|
+
contextOptions.recordVideo = {
|
|
767
|
+
dir: opts.capture.video.dir,
|
|
768
|
+
size: opts.capture.video.sizeWidth && opts.capture.video.sizeHeight ? {
|
|
769
|
+
width: opts.capture.video.sizeWidth,
|
|
770
|
+
height: opts.capture.video.sizeHeight
|
|
771
|
+
} : void 0
|
|
772
|
+
};
|
|
773
|
+
}
|
|
656
774
|
const context = await browser.newContext(contextOptions);
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
775
|
+
if (opts.capture?.trace) {
|
|
776
|
+
await context.tracing.start({
|
|
777
|
+
screenshots: true,
|
|
778
|
+
snapshots: true,
|
|
779
|
+
sources: false
|
|
780
|
+
});
|
|
660
781
|
}
|
|
782
|
+
const page = await context.newPage();
|
|
661
783
|
const sessionId = randomUUID3();
|
|
662
|
-
const
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
session,
|
|
784
|
+
const internals = {
|
|
785
|
+
session: {
|
|
786
|
+
id: sessionId,
|
|
787
|
+
platform: "web",
|
|
788
|
+
browser,
|
|
789
|
+
context,
|
|
790
|
+
mainPage: page
|
|
791
|
+
},
|
|
671
792
|
refIndex: /* @__PURE__ */ new Map(),
|
|
672
793
|
snapshotGeneration: 0,
|
|
673
794
|
refGeneration: -1,
|
|
674
|
-
lastSnapshotAt: null
|
|
795
|
+
lastSnapshotAt: null,
|
|
796
|
+
pages: [page],
|
|
797
|
+
activePageIndex: 0,
|
|
798
|
+
consoleBuffer: [],
|
|
799
|
+
networkBuffer: [],
|
|
800
|
+
networkInflight: /* @__PURE__ */ new Map(),
|
|
801
|
+
networkNextId: 1,
|
|
802
|
+
dialogArming: null,
|
|
803
|
+
captureOpts: opts.capture,
|
|
804
|
+
traceStarted: !!opts.capture?.trace
|
|
805
|
+
};
|
|
806
|
+
this.attachPageListeners(internals, page);
|
|
807
|
+
context.on("page", (newPage) => {
|
|
808
|
+
internals.pages.push(newPage);
|
|
809
|
+
this.attachPageListeners(internals, newPage);
|
|
675
810
|
});
|
|
811
|
+
if (opts.url) {
|
|
812
|
+
await page.goto(opts.url, { waitUntil: "domcontentloaded" });
|
|
813
|
+
}
|
|
814
|
+
this.sessions.set(sessionId, internals);
|
|
676
815
|
log.info("session opened", {
|
|
677
816
|
session_id: sessionId,
|
|
678
817
|
browser: browserName,
|
|
679
|
-
url: opts.url ?? null
|
|
818
|
+
url: opts.url ?? null,
|
|
819
|
+
capture: opts.capture ? Object.keys(opts.capture).filter(
|
|
820
|
+
(k) => opts.capture[k]
|
|
821
|
+
) : []
|
|
680
822
|
});
|
|
681
823
|
return { id: sessionId, platform: "web" };
|
|
682
824
|
}
|
|
683
825
|
async close(session) {
|
|
684
826
|
const s = this.requireSession(session.id);
|
|
827
|
+
if (s.traceStarted && s.captureOpts?.trace) {
|
|
828
|
+
const tracePath = resolvePath(s.captureOpts.trace.artifactDir, "trace.zip");
|
|
829
|
+
await s.session.context.tracing.stop({ path: tracePath }).catch((err) => {
|
|
830
|
+
log.warn("trace stop failed", {
|
|
831
|
+
session_id: session.id,
|
|
832
|
+
err: String(err)
|
|
833
|
+
});
|
|
834
|
+
});
|
|
835
|
+
}
|
|
685
836
|
await s.session.context.close().catch((err) => {
|
|
686
837
|
log.warn("context close failed", { session_id: session.id, err: String(err) });
|
|
687
838
|
});
|
|
@@ -693,7 +844,7 @@ var PlaywrightEngine = class {
|
|
|
693
844
|
}
|
|
694
845
|
async snapshot(session, mode = "visible") {
|
|
695
846
|
const s = this.requireSession(session.id);
|
|
696
|
-
const ariaYaml = await s.
|
|
847
|
+
const ariaYaml = await this.activePage(s).ariaSnapshot({ mode: "ai" });
|
|
697
848
|
const { tree, refIndex } = parseAriaSnapshot(ariaYaml);
|
|
698
849
|
void mode;
|
|
699
850
|
s.snapshotGeneration += 1;
|
|
@@ -703,7 +854,7 @@ var PlaywrightEngine = class {
|
|
|
703
854
|
return {
|
|
704
855
|
session_id: session.id,
|
|
705
856
|
platform: "web",
|
|
706
|
-
url_or_screen: s.
|
|
857
|
+
url_or_screen: this.activePage(s).url(),
|
|
707
858
|
taken_at: s.lastSnapshotAt,
|
|
708
859
|
tree
|
|
709
860
|
};
|
|
@@ -723,7 +874,7 @@ var PlaywrightEngine = class {
|
|
|
723
874
|
}
|
|
724
875
|
async key(session, key) {
|
|
725
876
|
const s = this.requireSession(session.id);
|
|
726
|
-
await s.
|
|
877
|
+
await this.activePage(s).keyboard.press(key);
|
|
727
878
|
this.invalidateRefs(s);
|
|
728
879
|
}
|
|
729
880
|
async scroll(session, dir, amount = 400, ref) {
|
|
@@ -737,13 +888,13 @@ var PlaywrightEngine = class {
|
|
|
737
888
|
[dx, dy]
|
|
738
889
|
);
|
|
739
890
|
} else {
|
|
740
|
-
await s.
|
|
891
|
+
await this.activePage(s).mouse.wheel(dx, dy);
|
|
741
892
|
}
|
|
742
893
|
this.invalidateRefs(s);
|
|
743
894
|
}
|
|
744
895
|
async waitFor(session, cond, timeoutMs = 1e4) {
|
|
745
896
|
const s = this.requireSession(session.id);
|
|
746
|
-
const page = s
|
|
897
|
+
const page = this.activePage(s);
|
|
747
898
|
switch (cond.kind) {
|
|
748
899
|
case "text_visible":
|
|
749
900
|
await page.getByText(cond.text, { exact: false }).first().waitFor({ state: "visible", timeout: timeoutMs });
|
|
@@ -763,14 +914,14 @@ var PlaywrightEngine = class {
|
|
|
763
914
|
}
|
|
764
915
|
async screenshot(session, fullPage = false) {
|
|
765
916
|
const s = this.requireSession(session.id);
|
|
766
|
-
return s.
|
|
917
|
+
return this.activePage(s).screenshot({ fullPage });
|
|
767
918
|
}
|
|
768
919
|
async navigate(session, url) {
|
|
769
920
|
const s = this.requireSession(session.id);
|
|
770
921
|
if (s.session.platform !== "web") {
|
|
771
922
|
throw new UnsupportedPlatformError(s.session.platform);
|
|
772
923
|
}
|
|
773
|
-
await s.
|
|
924
|
+
await this.activePage(s).goto(url, { waitUntil: "domcontentloaded" });
|
|
774
925
|
this.invalidateRefs(s);
|
|
775
926
|
}
|
|
776
927
|
/**
|
|
@@ -784,7 +935,7 @@ var PlaywrightEngine = class {
|
|
|
784
935
|
if (s.session.platform !== "web") {
|
|
785
936
|
throw new UnsupportedPlatformError(s.session.platform);
|
|
786
937
|
}
|
|
787
|
-
return s
|
|
938
|
+
return this.activePage(s);
|
|
788
939
|
}
|
|
789
940
|
/** Increment generation; the next ref-using call will see them as stale. */
|
|
790
941
|
bumpGeneration(sessionId) {
|
|
@@ -792,8 +943,311 @@ var PlaywrightEngine = class {
|
|
|
792
943
|
this.invalidateRefs(s);
|
|
793
944
|
}
|
|
794
945
|
// -------------------------------------------------------------------------
|
|
946
|
+
// v0.5 — input additions
|
|
947
|
+
// -------------------------------------------------------------------------
|
|
948
|
+
async hover(session, ref) {
|
|
949
|
+
const s = this.requireSession(session.id);
|
|
950
|
+
const locator = this.resolveLocator(s, ref);
|
|
951
|
+
await locator.hover();
|
|
952
|
+
}
|
|
953
|
+
async drag(session, fromRef, toRef) {
|
|
954
|
+
const s = this.requireSession(session.id);
|
|
955
|
+
const from = this.resolveLocator(s, fromRef);
|
|
956
|
+
const to = this.resolveLocator(s, toRef);
|
|
957
|
+
await from.dragTo(to);
|
|
958
|
+
this.invalidateRefs(s);
|
|
959
|
+
}
|
|
960
|
+
async fillForm(session, fields) {
|
|
961
|
+
const s = this.requireSession(session.id);
|
|
962
|
+
for (const field of fields) {
|
|
963
|
+
const locator = this.resolveLocator(s, field.ref);
|
|
964
|
+
const kind = field.kind;
|
|
965
|
+
if (kind === "checkbox" || kind === "radio") {
|
|
966
|
+
const checked = typeof field.value === "boolean" ? field.value : field.value === "true" || field.value === "on";
|
|
967
|
+
await locator.setChecked(checked);
|
|
968
|
+
} else if (kind === "select") {
|
|
969
|
+
await locator.selectOption(String(field.value));
|
|
970
|
+
} else {
|
|
971
|
+
await locator.fill(String(field.value));
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
this.invalidateRefs(s);
|
|
975
|
+
}
|
|
976
|
+
async uploadFile(session, ref, filePath) {
|
|
977
|
+
const s = this.requireSession(session.id);
|
|
978
|
+
if (!isAbsolute(filePath)) {
|
|
979
|
+
throw new RolepodMcpError(
|
|
980
|
+
"invalid_input",
|
|
981
|
+
`upload_file requires an absolute path; got "${filePath}".`,
|
|
982
|
+
{ file_path: filePath }
|
|
983
|
+
);
|
|
984
|
+
}
|
|
985
|
+
const locator = this.resolveLocator(s, ref);
|
|
986
|
+
await locator.setInputFiles(filePath);
|
|
987
|
+
this.invalidateRefs(s);
|
|
988
|
+
}
|
|
989
|
+
// -------------------------------------------------------------------------
|
|
990
|
+
// v0.5 — web-only extensions (not on Engine interface; tools cast to
|
|
991
|
+
// PlaywrightEngine before calling).
|
|
992
|
+
// -------------------------------------------------------------------------
|
|
993
|
+
/**
|
|
994
|
+
* Pre-arm a one-shot dialog handler for the next dialog raised on the
|
|
995
|
+
* active page. Returns when either the dialog fires (and is handled)
|
|
996
|
+
* or the timeout elapses. The caller is expected to trigger the
|
|
997
|
+
* dialog (via click etc.) AFTER arming.
|
|
998
|
+
*/
|
|
999
|
+
async handleDialog(sessionId, opts) {
|
|
1000
|
+
const s = this.requireSession(sessionId);
|
|
1001
|
+
const timeoutMs = opts.timeoutMs ?? 3e4;
|
|
1002
|
+
const expiresAt = Date.now() + timeoutMs;
|
|
1003
|
+
if (s.dialogArming) {
|
|
1004
|
+
s.dialogArming.resolve(false);
|
|
1005
|
+
}
|
|
1006
|
+
return new Promise((resolve4) => {
|
|
1007
|
+
const arming = {
|
|
1008
|
+
action: opts.action,
|
|
1009
|
+
text: opts.text,
|
|
1010
|
+
expiresAt,
|
|
1011
|
+
resolve: (handled) => {
|
|
1012
|
+
s.dialogArming = null;
|
|
1013
|
+
resolve4({ handled });
|
|
1014
|
+
}
|
|
1015
|
+
};
|
|
1016
|
+
s.dialogArming = arming;
|
|
1017
|
+
const timer = setTimeout(() => {
|
|
1018
|
+
if (s.dialogArming === arming) {
|
|
1019
|
+
s.dialogArming = null;
|
|
1020
|
+
resolve4({ handled: false });
|
|
1021
|
+
}
|
|
1022
|
+
}, timeoutMs);
|
|
1023
|
+
timer.unref?.();
|
|
1024
|
+
});
|
|
1025
|
+
}
|
|
1026
|
+
getConsole(sessionId, opts) {
|
|
1027
|
+
const s = this.requireSession(sessionId);
|
|
1028
|
+
const levels = opts?.levels;
|
|
1029
|
+
const contains = opts?.contains;
|
|
1030
|
+
const limit = opts?.limit ?? 50;
|
|
1031
|
+
let entries = s.consoleBuffer;
|
|
1032
|
+
if (levels && levels.length > 0) {
|
|
1033
|
+
entries = entries.filter((e) => levels.includes(e.level));
|
|
1034
|
+
}
|
|
1035
|
+
if (contains) {
|
|
1036
|
+
entries = entries.filter((e) => e.text.includes(contains));
|
|
1037
|
+
}
|
|
1038
|
+
const result = entries.slice(-limit);
|
|
1039
|
+
if (opts?.clear) s.consoleBuffer = [];
|
|
1040
|
+
return result;
|
|
1041
|
+
}
|
|
1042
|
+
getNetwork(sessionId, opts) {
|
|
1043
|
+
const s = this.requireSession(sessionId);
|
|
1044
|
+
let entries = s.networkBuffer;
|
|
1045
|
+
if (opts?.urlPattern) {
|
|
1046
|
+
if (opts.patternKind === "regex") {
|
|
1047
|
+
const re = new RegExp(opts.urlPattern);
|
|
1048
|
+
entries = entries.filter((e) => re.test(e.url));
|
|
1049
|
+
} else {
|
|
1050
|
+
entries = entries.filter((e) => e.url.includes(opts.urlPattern));
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
if (opts?.method) {
|
|
1054
|
+
const m = opts.method.toUpperCase();
|
|
1055
|
+
entries = entries.filter((e) => e.method.toUpperCase() === m);
|
|
1056
|
+
}
|
|
1057
|
+
if (opts?.statusRange) {
|
|
1058
|
+
const { min, max } = opts.statusRange;
|
|
1059
|
+
entries = entries.filter(
|
|
1060
|
+
(e) => e.status !== void 0 && e.status >= min && e.status <= max
|
|
1061
|
+
);
|
|
1062
|
+
}
|
|
1063
|
+
if (opts?.onlyFailed) {
|
|
1064
|
+
entries = entries.filter(
|
|
1065
|
+
(e) => !!e.failure || e.status !== void 0 && e.status >= 400
|
|
1066
|
+
);
|
|
1067
|
+
}
|
|
1068
|
+
const limit = opts?.limit ?? 50;
|
|
1069
|
+
const result = entries.slice(-limit);
|
|
1070
|
+
if (opts?.clear) s.networkBuffer = [];
|
|
1071
|
+
return result;
|
|
1072
|
+
}
|
|
1073
|
+
/**
|
|
1074
|
+
* Read the consoleBuffer/networkBuffer directly without filtering —
|
|
1075
|
+
* used by verify_ui_flow expect evaluators.
|
|
1076
|
+
*/
|
|
1077
|
+
peekBuffers(sessionId) {
|
|
1078
|
+
const s = this.requireSession(sessionId);
|
|
1079
|
+
return { console: s.consoleBuffer, network: s.networkBuffer };
|
|
1080
|
+
}
|
|
1081
|
+
/**
|
|
1082
|
+
* Runtime mutation of context-level emulation. CPU + network throttle
|
|
1083
|
+
* use CDP and only work on chromium; everything else is cross-browser.
|
|
1084
|
+
*/
|
|
1085
|
+
async setEnv(sessionId, opts) {
|
|
1086
|
+
const s = this.requireSession(sessionId);
|
|
1087
|
+
const page = this.activePage(s);
|
|
1088
|
+
const ctx = s.session.context;
|
|
1089
|
+
if (opts.viewport) {
|
|
1090
|
+
await page.setViewportSize(opts.viewport);
|
|
1091
|
+
}
|
|
1092
|
+
if (opts.offline !== void 0) {
|
|
1093
|
+
await ctx.setOffline(opts.offline);
|
|
1094
|
+
}
|
|
1095
|
+
if (opts.geolocation) {
|
|
1096
|
+
await ctx.setGeolocation(opts.geolocation);
|
|
1097
|
+
}
|
|
1098
|
+
if (opts.extraHeaders) {
|
|
1099
|
+
await ctx.setExtraHTTPHeaders(opts.extraHeaders);
|
|
1100
|
+
}
|
|
1101
|
+
if (opts.colorScheme || opts.reducedMotion) {
|
|
1102
|
+
await page.emulateMedia({
|
|
1103
|
+
...opts.colorScheme ? { colorScheme: opts.colorScheme } : {},
|
|
1104
|
+
...opts.reducedMotion ? { reducedMotion: opts.reducedMotion } : {}
|
|
1105
|
+
});
|
|
1106
|
+
}
|
|
1107
|
+
if (opts.networkThrottle || opts.cpuThrottle !== void 0) {
|
|
1108
|
+
const browserName = ctx.browser()?.browserType().name();
|
|
1109
|
+
if (browserName !== "chromium") {
|
|
1110
|
+
throw new RolepodMcpError(
|
|
1111
|
+
"unsupported_engine",
|
|
1112
|
+
`networkThrottle / cpuThrottle require chromium (CDP-backed); current browser is "${browserName}".`
|
|
1113
|
+
);
|
|
1114
|
+
}
|
|
1115
|
+
const cdp = await ctx.newCDPSession(page);
|
|
1116
|
+
try {
|
|
1117
|
+
if (opts.networkThrottle) {
|
|
1118
|
+
const preset = NETWORK_PRESETS[opts.networkThrottle];
|
|
1119
|
+
await cdp.send("Network.enable");
|
|
1120
|
+
await cdp.send("Network.emulateNetworkConditions", preset);
|
|
1121
|
+
}
|
|
1122
|
+
if (opts.cpuThrottle !== void 0) {
|
|
1123
|
+
await cdp.send("Emulation.setCPUThrottlingRate", {
|
|
1124
|
+
rate: opts.cpuThrottle
|
|
1125
|
+
});
|
|
1126
|
+
}
|
|
1127
|
+
} finally {
|
|
1128
|
+
await cdp.detach().catch(() => void 0);
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
this.invalidateRefs(s);
|
|
1132
|
+
}
|
|
1133
|
+
/**
|
|
1134
|
+
* Execute a JavaScript function in the page context. ALWAYS gated by
|
|
1135
|
+
* the tool layer (`ROLEPOD_ALLOW_EVAL=1`); this method does not enforce
|
|
1136
|
+
* the env check.
|
|
1137
|
+
*/
|
|
1138
|
+
async evaluate(sessionId, script, args) {
|
|
1139
|
+
const s = this.requireSession(sessionId);
|
|
1140
|
+
const page = this.activePage(s);
|
|
1141
|
+
return page.evaluate(
|
|
1142
|
+
({ src, a }) => (
|
|
1143
|
+
// eslint-disable-next-line no-new-func
|
|
1144
|
+
new Function("args", `return (async () => { ${src} })();`)(a)
|
|
1145
|
+
),
|
|
1146
|
+
{ src: script, a: args ?? [] }
|
|
1147
|
+
);
|
|
1148
|
+
}
|
|
1149
|
+
listPages(sessionId) {
|
|
1150
|
+
const s = this.requireSession(sessionId);
|
|
1151
|
+
return s.pages.map((p, i) => ({
|
|
1152
|
+
index: i,
|
|
1153
|
+
url: p.url(),
|
|
1154
|
+
title_promise: p.title(),
|
|
1155
|
+
active: i === s.activePageIndex
|
|
1156
|
+
}));
|
|
1157
|
+
}
|
|
1158
|
+
async switchPage(sessionId, index) {
|
|
1159
|
+
const s = this.requireSession(sessionId);
|
|
1160
|
+
if (index < 0 || index >= s.pages.length) {
|
|
1161
|
+
throw new RolepodMcpError(
|
|
1162
|
+
"invalid_input",
|
|
1163
|
+
`Page index ${index} out of range (have ${s.pages.length} page(s)).`,
|
|
1164
|
+
{ index, available: s.pages.length }
|
|
1165
|
+
);
|
|
1166
|
+
}
|
|
1167
|
+
s.activePageIndex = index;
|
|
1168
|
+
this.invalidateRefs(s);
|
|
1169
|
+
}
|
|
1170
|
+
// -------------------------------------------------------------------------
|
|
795
1171
|
// Internal helpers
|
|
796
1172
|
// -------------------------------------------------------------------------
|
|
1173
|
+
activePage(s) {
|
|
1174
|
+
return s.pages[s.activePageIndex] ?? s.session.mainPage;
|
|
1175
|
+
}
|
|
1176
|
+
attachPageListeners(s, page) {
|
|
1177
|
+
page.on("console", (msg) => {
|
|
1178
|
+
const level = mapConsoleLevel(msg.type());
|
|
1179
|
+
pushRing(
|
|
1180
|
+
s.consoleBuffer,
|
|
1181
|
+
{
|
|
1182
|
+
level,
|
|
1183
|
+
text: msg.text(),
|
|
1184
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1185
|
+
location: formatConsoleLocation(msg)
|
|
1186
|
+
},
|
|
1187
|
+
CONSOLE_BUFFER_CAP
|
|
1188
|
+
);
|
|
1189
|
+
});
|
|
1190
|
+
page.on("request", (req) => {
|
|
1191
|
+
const id = s.networkNextId++;
|
|
1192
|
+
s.networkInflight.set(req.url() + "::" + req.method() + "::" + id, {
|
|
1193
|
+
id,
|
|
1194
|
+
startedAt: Date.now(),
|
|
1195
|
+
resourceType: req.resourceType()
|
|
1196
|
+
});
|
|
1197
|
+
pushRing(
|
|
1198
|
+
s.networkBuffer,
|
|
1199
|
+
{
|
|
1200
|
+
id,
|
|
1201
|
+
url: req.url(),
|
|
1202
|
+
method: req.method(),
|
|
1203
|
+
resource_type: req.resourceType(),
|
|
1204
|
+
ts: (/* @__PURE__ */ new Date()).toISOString()
|
|
1205
|
+
},
|
|
1206
|
+
NETWORK_BUFFER_CAP
|
|
1207
|
+
);
|
|
1208
|
+
});
|
|
1209
|
+
page.on("response", (res) => {
|
|
1210
|
+
const req = res.request();
|
|
1211
|
+
const entry = findNetworkEntry(s.networkBuffer, req);
|
|
1212
|
+
if (entry) {
|
|
1213
|
+
entry.status = res.status();
|
|
1214
|
+
entry.duration_ms = Date.now() - new Date(entry.ts).getTime();
|
|
1215
|
+
}
|
|
1216
|
+
});
|
|
1217
|
+
page.on("requestfailed", (req) => {
|
|
1218
|
+
const entry = findNetworkEntry(s.networkBuffer, req);
|
|
1219
|
+
if (entry) {
|
|
1220
|
+
entry.failure = req.failure()?.errorText ?? "request failed";
|
|
1221
|
+
}
|
|
1222
|
+
});
|
|
1223
|
+
page.on("dialog", (dialog) => {
|
|
1224
|
+
void this.handlePageDialog(s, dialog);
|
|
1225
|
+
});
|
|
1226
|
+
}
|
|
1227
|
+
async handlePageDialog(s, dialog) {
|
|
1228
|
+
const arm = s.dialogArming;
|
|
1229
|
+
if (!arm || Date.now() > arm.expiresAt) {
|
|
1230
|
+
await dialog.dismiss().catch(() => void 0);
|
|
1231
|
+
if (arm) arm.resolve(false);
|
|
1232
|
+
return;
|
|
1233
|
+
}
|
|
1234
|
+
try {
|
|
1235
|
+
if (arm.action === "accept") {
|
|
1236
|
+
await dialog.accept();
|
|
1237
|
+
} else if (arm.action === "accept_with_text") {
|
|
1238
|
+
await dialog.accept(arm.text ?? "");
|
|
1239
|
+
} else {
|
|
1240
|
+
await dialog.dismiss();
|
|
1241
|
+
}
|
|
1242
|
+
arm.resolve(true);
|
|
1243
|
+
} catch (err) {
|
|
1244
|
+
log.warn("dialog handle failed", {
|
|
1245
|
+
session_id: s.session.id,
|
|
1246
|
+
err: String(err)
|
|
1247
|
+
});
|
|
1248
|
+
arm.resolve(false);
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
797
1251
|
requireSession(sessionId) {
|
|
798
1252
|
const s = this.sessions.get(sessionId);
|
|
799
1253
|
if (!s) {
|
|
@@ -822,7 +1276,7 @@ var PlaywrightEngine = class {
|
|
|
822
1276
|
if (meta.ref.startsWith("s")) {
|
|
823
1277
|
throw new UnknownRefError(s.session.id, ref);
|
|
824
1278
|
}
|
|
825
|
-
return s.
|
|
1279
|
+
return this.activePage(s).locator(`aria-ref=${meta.ref}`);
|
|
826
1280
|
}
|
|
827
1281
|
invalidateRefs(s) {
|
|
828
1282
|
s.snapshotGeneration += 1;
|
|
@@ -1064,6 +1518,143 @@ var browserNavigateShape = {
|
|
|
1064
1518
|
url: z.string().url()
|
|
1065
1519
|
};
|
|
1066
1520
|
var browserNavigateSchema = z.object(browserNavigateShape);
|
|
1521
|
+
var browserHoverShape = {
|
|
1522
|
+
session_id: z.string().min(1),
|
|
1523
|
+
ref: z.string().min(1)
|
|
1524
|
+
};
|
|
1525
|
+
var browserHoverSchema = z.object(browserHoverShape);
|
|
1526
|
+
var browserDragShape = {
|
|
1527
|
+
session_id: z.string().min(1),
|
|
1528
|
+
from_ref: z.string().min(1),
|
|
1529
|
+
to_ref: z.string().min(1)
|
|
1530
|
+
};
|
|
1531
|
+
var browserDragSchema = z.object(browserDragShape);
|
|
1532
|
+
var fillFieldKindSchema = z.enum([
|
|
1533
|
+
"input",
|
|
1534
|
+
"select",
|
|
1535
|
+
"checkbox",
|
|
1536
|
+
"radio"
|
|
1537
|
+
]);
|
|
1538
|
+
var fillFormFieldSchema = z.object({
|
|
1539
|
+
ref: z.string().min(1),
|
|
1540
|
+
value: z.union([z.string(), z.boolean()]),
|
|
1541
|
+
kind: fillFieldKindSchema.optional()
|
|
1542
|
+
});
|
|
1543
|
+
var browserFillFormShape = {
|
|
1544
|
+
session_id: z.string().min(1),
|
|
1545
|
+
fields: z.array(fillFormFieldSchema).min(1)
|
|
1546
|
+
};
|
|
1547
|
+
var browserFillFormSchema = z.object(browserFillFormShape);
|
|
1548
|
+
var browserUploadFileShape = {
|
|
1549
|
+
session_id: z.string().min(1),
|
|
1550
|
+
ref: z.string().min(1),
|
|
1551
|
+
file_path: z.string().min(1)
|
|
1552
|
+
};
|
|
1553
|
+
var browserUploadFileSchema = z.object(browserUploadFileShape);
|
|
1554
|
+
var dialogActionSchema = z.enum([
|
|
1555
|
+
"accept",
|
|
1556
|
+
"dismiss",
|
|
1557
|
+
"accept_with_text"
|
|
1558
|
+
]);
|
|
1559
|
+
var browserHandleDialogShape = {
|
|
1560
|
+
session_id: z.string().min(1),
|
|
1561
|
+
action: dialogActionSchema,
|
|
1562
|
+
/** Only used when action='accept_with_text'. */
|
|
1563
|
+
text: z.string().optional(),
|
|
1564
|
+
/**
|
|
1565
|
+
* Arming behavior: registers a one-shot handler for the NEXT dialog
|
|
1566
|
+
* raised on the page. Call this BEFORE the action that triggers the
|
|
1567
|
+
* dialog (e.g. before clicking the button that calls `confirm()`).
|
|
1568
|
+
* Default 30s if no dialog appears, handler is auto-removed.
|
|
1569
|
+
*/
|
|
1570
|
+
timeout_ms: z.number().int().positive().optional()
|
|
1571
|
+
};
|
|
1572
|
+
var browserHandleDialogSchema = z.object(browserHandleDialogShape);
|
|
1573
|
+
var consoleLevelSchema = z.enum([
|
|
1574
|
+
"error",
|
|
1575
|
+
"warning",
|
|
1576
|
+
"info",
|
|
1577
|
+
"log",
|
|
1578
|
+
"debug",
|
|
1579
|
+
"trace"
|
|
1580
|
+
]);
|
|
1581
|
+
var browserConsoleShape = {
|
|
1582
|
+
session_id: z.string().min(1),
|
|
1583
|
+
/** Filter to only these levels. Default: errors+warnings. */
|
|
1584
|
+
levels: z.array(consoleLevelSchema).optional(),
|
|
1585
|
+
/** Substring match on message text. */
|
|
1586
|
+
contains: z.string().optional(),
|
|
1587
|
+
/** Drop all buffered messages after returning. */
|
|
1588
|
+
clear: z.boolean().default(false),
|
|
1589
|
+
/** Cap on returned messages (artifact still holds full ring buffer). */
|
|
1590
|
+
limit: z.number().int().positive().max(1e3).default(50)
|
|
1591
|
+
};
|
|
1592
|
+
var browserConsoleSchema = z.object(browserConsoleShape);
|
|
1593
|
+
var browserNetworkShape = {
|
|
1594
|
+
session_id: z.string().min(1),
|
|
1595
|
+
/** Substring or regex (per `pattern_kind`) match on URL. */
|
|
1596
|
+
url_pattern: z.string().optional(),
|
|
1597
|
+
pattern_kind: z.enum(["substring", "regex"]).default("substring"),
|
|
1598
|
+
method: z.string().optional(),
|
|
1599
|
+
/** Inclusive range — e.g. `{min: 400, max: 599}` for any error response. */
|
|
1600
|
+
status_range: z.object({
|
|
1601
|
+
min: z.number().int().min(100).max(599),
|
|
1602
|
+
max: z.number().int().min(100).max(599)
|
|
1603
|
+
}).optional(),
|
|
1604
|
+
only_failed: z.boolean().default(false),
|
|
1605
|
+
/** Write the full HAR file for this session to artifacts/{runId}/network.har. */
|
|
1606
|
+
export_har: z.boolean().default(false),
|
|
1607
|
+
/** Drop buffered entries after returning. */
|
|
1608
|
+
clear: z.boolean().default(false),
|
|
1609
|
+
limit: z.number().int().positive().max(1e3).default(50)
|
|
1610
|
+
};
|
|
1611
|
+
var browserNetworkSchema = z.object(browserNetworkShape);
|
|
1612
|
+
var networkPresetSchema = z.enum([
|
|
1613
|
+
"offline",
|
|
1614
|
+
"slow-3g",
|
|
1615
|
+
"fast-3g",
|
|
1616
|
+
"slow-4g",
|
|
1617
|
+
"fast-4g",
|
|
1618
|
+
"no-throttling"
|
|
1619
|
+
]);
|
|
1620
|
+
var geolocationSchema = z.object({
|
|
1621
|
+
latitude: z.number().min(-90).max(90),
|
|
1622
|
+
longitude: z.number().min(-180).max(180),
|
|
1623
|
+
accuracy: z.number().nonnegative().optional()
|
|
1624
|
+
});
|
|
1625
|
+
var browserSetEnvShape = {
|
|
1626
|
+
session_id: z.string().min(1),
|
|
1627
|
+
viewport: viewportSchema.optional(),
|
|
1628
|
+
offline: z.boolean().optional(),
|
|
1629
|
+
geolocation: geolocationSchema.optional(),
|
|
1630
|
+
color_scheme: z.enum(["light", "dark", "no-preference"]).optional(),
|
|
1631
|
+
reduced_motion: z.enum(["reduce", "no-preference"]).optional(),
|
|
1632
|
+
extra_headers: z.record(z.string(), z.string()).optional(),
|
|
1633
|
+
network_throttle: networkPresetSchema.optional(),
|
|
1634
|
+
/** CPU slowdown multiplier (1 = no throttle, 4 = 4x slower). Chromium only. */
|
|
1635
|
+
cpu_throttle: z.number().min(1).max(20).optional()
|
|
1636
|
+
};
|
|
1637
|
+
var browserSetEnvSchema = z.object(browserSetEnvShape);
|
|
1638
|
+
var browserEvaluateShape = {
|
|
1639
|
+
session_id: z.string().min(1),
|
|
1640
|
+
script: z.string().min(1),
|
|
1641
|
+
args: z.array(z.unknown()).optional()
|
|
1642
|
+
};
|
|
1643
|
+
var browserEvaluateSchema = z.object(browserEvaluateShape);
|
|
1644
|
+
var browserPagesShape = {
|
|
1645
|
+
session_id: z.string().min(1)
|
|
1646
|
+
};
|
|
1647
|
+
var browserPagesSchema = z.object(browserPagesShape);
|
|
1648
|
+
var browserSwitchPageShape = {
|
|
1649
|
+
session_id: z.string().min(1),
|
|
1650
|
+
index: z.number().int().nonnegative()
|
|
1651
|
+
};
|
|
1652
|
+
var browserSwitchPageSchema = z.object(browserSwitchPageShape);
|
|
1653
|
+
var verifyFillFieldSchema = z.object({
|
|
1654
|
+
query: z.string(),
|
|
1655
|
+
value: z.union([z.string(), z.boolean()]),
|
|
1656
|
+
kind: fillFieldKindSchema.optional()
|
|
1657
|
+
});
|
|
1067
1658
|
var verifyStepSchema = z.discriminatedUnion("kind", [
|
|
1068
1659
|
z.object({ kind: z.literal("click"), query: z.string() }),
|
|
1069
1660
|
z.object({
|
|
@@ -1074,7 +1665,44 @@ var verifyStepSchema = z.discriminatedUnion("kind", [
|
|
|
1074
1665
|
}),
|
|
1075
1666
|
z.object({ kind: z.literal("key"), key: z.string() }),
|
|
1076
1667
|
z.object({ kind: z.literal("wait_for"), condition: waitConditionSchema }),
|
|
1077
|
-
z.object({ kind: z.literal("navigate"), url: z.string().url() })
|
|
1668
|
+
z.object({ kind: z.literal("navigate"), url: z.string().url() }),
|
|
1669
|
+
// v0.5 additions
|
|
1670
|
+
z.object({ kind: z.literal("hover"), query: z.string() }),
|
|
1671
|
+
z.object({
|
|
1672
|
+
kind: z.literal("drag"),
|
|
1673
|
+
from_query: z.string(),
|
|
1674
|
+
to_query: z.string()
|
|
1675
|
+
}),
|
|
1676
|
+
z.object({
|
|
1677
|
+
kind: z.literal("fill_form"),
|
|
1678
|
+
fields: z.array(verifyFillFieldSchema).min(1)
|
|
1679
|
+
}),
|
|
1680
|
+
z.object({
|
|
1681
|
+
kind: z.literal("upload"),
|
|
1682
|
+
query: z.string(),
|
|
1683
|
+
file_path: z.string().min(1)
|
|
1684
|
+
}),
|
|
1685
|
+
z.object({
|
|
1686
|
+
kind: z.literal("dialog"),
|
|
1687
|
+
action: dialogActionSchema,
|
|
1688
|
+
text: z.string().optional()
|
|
1689
|
+
}),
|
|
1690
|
+
z.object({
|
|
1691
|
+
kind: z.literal("set_env"),
|
|
1692
|
+
viewport: viewportSchema.optional(),
|
|
1693
|
+
offline: z.boolean().optional(),
|
|
1694
|
+
geolocation: geolocationSchema.optional(),
|
|
1695
|
+
color_scheme: z.enum(["light", "dark", "no-preference"]).optional(),
|
|
1696
|
+
reduced_motion: z.enum(["reduce", "no-preference"]).optional(),
|
|
1697
|
+
extra_headers: z.record(z.string(), z.string()).optional(),
|
|
1698
|
+
network_throttle: networkPresetSchema.optional(),
|
|
1699
|
+
cpu_throttle: z.number().min(1).max(20).optional()
|
|
1700
|
+
}),
|
|
1701
|
+
z.object({
|
|
1702
|
+
kind: z.literal("switch_page"),
|
|
1703
|
+
index: z.number().int().nonnegative()
|
|
1704
|
+
}),
|
|
1705
|
+
z.object({ kind: z.literal("evaluate"), script: z.string().min(1) })
|
|
1078
1706
|
]);
|
|
1079
1707
|
var verifyExpectSchema = z.discriminatedUnion("kind", [
|
|
1080
1708
|
z.object({ kind: z.literal("text_visible"), text: z.string() }),
|
|
@@ -1084,6 +1712,28 @@ var verifyExpectSchema = z.discriminatedUnion("kind", [
|
|
|
1084
1712
|
kind: z.literal("ref_in_state"),
|
|
1085
1713
|
query: z.string(),
|
|
1086
1714
|
state: z.enum(["visible", "enabled", "focused"])
|
|
1715
|
+
}),
|
|
1716
|
+
// v0.5 additions
|
|
1717
|
+
z.object({
|
|
1718
|
+
kind: z.literal("no_console_errors"),
|
|
1719
|
+
exclude_patterns: z.array(z.string()).optional()
|
|
1720
|
+
}),
|
|
1721
|
+
z.object({
|
|
1722
|
+
kind: z.literal("no_failed_requests"),
|
|
1723
|
+
exclude_patterns: z.array(z.string()).optional(),
|
|
1724
|
+
/** When true, only 5xx counts as a failure. Default false (4xx + 5xx). */
|
|
1725
|
+
allow_4xx: z.boolean().optional()
|
|
1726
|
+
}),
|
|
1727
|
+
z.object({
|
|
1728
|
+
kind: z.literal("request_made"),
|
|
1729
|
+
url_pattern: z.string(),
|
|
1730
|
+
method: z.string().optional(),
|
|
1731
|
+
min_count: z.number().int().positive().optional()
|
|
1732
|
+
}),
|
|
1733
|
+
z.object({
|
|
1734
|
+
kind: z.literal("response_status"),
|
|
1735
|
+
url_pattern: z.string(),
|
|
1736
|
+
status: z.number().int().min(100).max(599)
|
|
1087
1737
|
})
|
|
1088
1738
|
]);
|
|
1089
1739
|
var captureKindSchema = z.enum([
|
|
@@ -1091,7 +1741,8 @@ var captureKindSchema = z.enum([
|
|
|
1091
1741
|
"har",
|
|
1092
1742
|
"console",
|
|
1093
1743
|
"a11y_tree",
|
|
1094
|
-
"video"
|
|
1744
|
+
"video",
|
|
1745
|
+
"trace"
|
|
1095
1746
|
]);
|
|
1096
1747
|
var verifyUiFlowShape = {
|
|
1097
1748
|
mode: z.enum(["assert", "reproduce"]).default("assert"),
|
|
@@ -1169,6 +1820,19 @@ var ToolNames = {
|
|
|
1169
1820
|
browserWaitFor: "rolepod_browser_wait_for",
|
|
1170
1821
|
browserScreenshot: "rolepod_browser_screenshot",
|
|
1171
1822
|
browserNavigate: "rolepod_browser_navigate",
|
|
1823
|
+
// v0.5 atomics
|
|
1824
|
+
browserHover: "rolepod_browser_hover",
|
|
1825
|
+
browserDrag: "rolepod_browser_drag",
|
|
1826
|
+
browserFillForm: "rolepod_browser_fill_form",
|
|
1827
|
+
browserUploadFile: "rolepod_browser_upload_file",
|
|
1828
|
+
browserHandleDialog: "rolepod_browser_handle_dialog",
|
|
1829
|
+
browserConsole: "rolepod_browser_console",
|
|
1830
|
+
browserNetwork: "rolepod_browser_network",
|
|
1831
|
+
browserSetEnv: "rolepod_browser_set_env",
|
|
1832
|
+
browserEvaluate: "rolepod_browser_evaluate",
|
|
1833
|
+
browserPages: "rolepod_browser_pages",
|
|
1834
|
+
browserSwitchPage: "rolepod_browser_switch_page",
|
|
1835
|
+
// composite
|
|
1172
1836
|
verifyUiFlow: "rolepod_verify_ui_flow",
|
|
1173
1837
|
auditA11y: "rolepod_audit_a11y",
|
|
1174
1838
|
visualDiff: "rolepod_visual_diff",
|
|
@@ -1242,6 +1906,151 @@ var browserCloseTool = {
|
|
|
1242
1906
|
}
|
|
1243
1907
|
};
|
|
1244
1908
|
|
|
1909
|
+
// src/tools/atomic/browser_console.ts
|
|
1910
|
+
var browserConsoleTool = {
|
|
1911
|
+
name: ToolNames.browserConsole,
|
|
1912
|
+
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.",
|
|
1913
|
+
inputShape: browserConsoleShape,
|
|
1914
|
+
build(ctx) {
|
|
1915
|
+
return safeHandler(async (args) => {
|
|
1916
|
+
const engine = ctx.registry.engineFor(args.session_id);
|
|
1917
|
+
if (!(engine instanceof PlaywrightEngine)) {
|
|
1918
|
+
throw new RolepodMcpError(
|
|
1919
|
+
"unsupported_engine",
|
|
1920
|
+
"console is web-only and requires PlaywrightEngine."
|
|
1921
|
+
);
|
|
1922
|
+
}
|
|
1923
|
+
const levels = args.levels && args.levels.length > 0 ? args.levels : ["error", "warning"];
|
|
1924
|
+
const messages = engine.getConsole(args.session_id, {
|
|
1925
|
+
levels,
|
|
1926
|
+
...args.contains !== void 0 ? { contains: args.contains } : {},
|
|
1927
|
+
clear: args.clear,
|
|
1928
|
+
limit: args.limit
|
|
1929
|
+
});
|
|
1930
|
+
return ok({
|
|
1931
|
+
count: messages.length,
|
|
1932
|
+
messages
|
|
1933
|
+
});
|
|
1934
|
+
});
|
|
1935
|
+
}
|
|
1936
|
+
};
|
|
1937
|
+
|
|
1938
|
+
// src/tools/atomic/browser_drag.ts
|
|
1939
|
+
var browserDragTool = {
|
|
1940
|
+
name: ToolNames.browserDrag,
|
|
1941
|
+
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.",
|
|
1942
|
+
inputShape: browserDragShape,
|
|
1943
|
+
build(ctx) {
|
|
1944
|
+
return safeHandler(async (args) => {
|
|
1945
|
+
const engine = ctx.registry.engineFor(args.session_id);
|
|
1946
|
+
await engine.drag(
|
|
1947
|
+
{
|
|
1948
|
+
id: args.session_id,
|
|
1949
|
+
platform: ctx.registry.platformOf(args.session_id)
|
|
1950
|
+
},
|
|
1951
|
+
args.from_ref,
|
|
1952
|
+
args.to_ref
|
|
1953
|
+
);
|
|
1954
|
+
return ok({ dragged: true });
|
|
1955
|
+
});
|
|
1956
|
+
}
|
|
1957
|
+
};
|
|
1958
|
+
|
|
1959
|
+
// src/tools/atomic/browser_evaluate.ts
|
|
1960
|
+
var browserEvaluateTool = {
|
|
1961
|
+
name: ToolNames.browserEvaluate,
|
|
1962
|
+
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).",
|
|
1963
|
+
inputShape: browserEvaluateShape,
|
|
1964
|
+
build(ctx) {
|
|
1965
|
+
const allowed = process.env.ROLEPOD_ALLOW_EVAL === "1";
|
|
1966
|
+
return safeHandler(async (args) => {
|
|
1967
|
+
if (!allowed) {
|
|
1968
|
+
throw new RolepodMcpError(
|
|
1969
|
+
"engine_error",
|
|
1970
|
+
"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."
|
|
1971
|
+
);
|
|
1972
|
+
}
|
|
1973
|
+
const engine = ctx.registry.engineFor(args.session_id);
|
|
1974
|
+
if (!(engine instanceof PlaywrightEngine)) {
|
|
1975
|
+
throw new RolepodMcpError(
|
|
1976
|
+
"unsupported_engine",
|
|
1977
|
+
"evaluate is web-only and requires PlaywrightEngine."
|
|
1978
|
+
);
|
|
1979
|
+
}
|
|
1980
|
+
const result = await engine.evaluate(
|
|
1981
|
+
args.session_id,
|
|
1982
|
+
args.script,
|
|
1983
|
+
args.args
|
|
1984
|
+
);
|
|
1985
|
+
return ok({ result });
|
|
1986
|
+
});
|
|
1987
|
+
}
|
|
1988
|
+
};
|
|
1989
|
+
|
|
1990
|
+
// src/tools/atomic/browser_fill_form.ts
|
|
1991
|
+
var browserFillFormTool = {
|
|
1992
|
+
name: ToolNames.browserFillForm,
|
|
1993
|
+
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.",
|
|
1994
|
+
inputShape: browserFillFormShape,
|
|
1995
|
+
build(ctx) {
|
|
1996
|
+
return safeHandler(async (args) => {
|
|
1997
|
+
const engine = ctx.registry.engineFor(args.session_id);
|
|
1998
|
+
await engine.fillForm(
|
|
1999
|
+
{
|
|
2000
|
+
id: args.session_id,
|
|
2001
|
+
platform: ctx.registry.platformOf(args.session_id)
|
|
2002
|
+
},
|
|
2003
|
+
args.fields
|
|
2004
|
+
);
|
|
2005
|
+
return ok({ filled: args.fields.length });
|
|
2006
|
+
});
|
|
2007
|
+
}
|
|
2008
|
+
};
|
|
2009
|
+
|
|
2010
|
+
// src/tools/atomic/browser_handle_dialog.ts
|
|
2011
|
+
var browserHandleDialogTool = {
|
|
2012
|
+
name: ToolNames.browserHandleDialog,
|
|
2013
|
+
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.",
|
|
2014
|
+
inputShape: browserHandleDialogShape,
|
|
2015
|
+
build(ctx) {
|
|
2016
|
+
return safeHandler(async (args) => {
|
|
2017
|
+
const engine = ctx.registry.engineFor(args.session_id);
|
|
2018
|
+
if (!(engine instanceof PlaywrightEngine)) {
|
|
2019
|
+
throw new RolepodMcpError(
|
|
2020
|
+
"unsupported_engine",
|
|
2021
|
+
"handle_dialog is web-only and requires PlaywrightEngine."
|
|
2022
|
+
);
|
|
2023
|
+
}
|
|
2024
|
+
const { handled } = await engine.handleDialog(args.session_id, {
|
|
2025
|
+
action: args.action,
|
|
2026
|
+
...args.text !== void 0 ? { text: args.text } : {},
|
|
2027
|
+
...args.timeout_ms !== void 0 ? { timeoutMs: args.timeout_ms } : {}
|
|
2028
|
+
});
|
|
2029
|
+
return ok({ handled, action: args.action });
|
|
2030
|
+
});
|
|
2031
|
+
}
|
|
2032
|
+
};
|
|
2033
|
+
|
|
2034
|
+
// src/tools/atomic/browser_hover.ts
|
|
2035
|
+
var browserHoverTool = {
|
|
2036
|
+
name: ToolNames.browserHover,
|
|
2037
|
+
description: "Hover the pointer over the element identified by `ref`. Refs remain valid afterwards (read-mostly).",
|
|
2038
|
+
inputShape: browserHoverShape,
|
|
2039
|
+
build(ctx) {
|
|
2040
|
+
return safeHandler(async (args) => {
|
|
2041
|
+
const engine = ctx.registry.engineFor(args.session_id);
|
|
2042
|
+
await engine.hover(
|
|
2043
|
+
{
|
|
2044
|
+
id: args.session_id,
|
|
2045
|
+
platform: ctx.registry.platformOf(args.session_id)
|
|
2046
|
+
},
|
|
2047
|
+
args.ref
|
|
2048
|
+
);
|
|
2049
|
+
return ok({ hovered: true });
|
|
2050
|
+
});
|
|
2051
|
+
}
|
|
2052
|
+
};
|
|
2053
|
+
|
|
1245
2054
|
// src/tools/atomic/browser_key.ts
|
|
1246
2055
|
var browserKeyTool = {
|
|
1247
2056
|
name: ToolNames.browserKey,
|
|
@@ -1270,6 +2079,46 @@ var browserNavigateTool = {
|
|
|
1270
2079
|
}
|
|
1271
2080
|
};
|
|
1272
2081
|
|
|
2082
|
+
// src/tools/atomic/browser_network.ts
|
|
2083
|
+
var browserNetworkTool = {
|
|
2084
|
+
name: ToolNames.browserNetwork,
|
|
2085
|
+
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`.',
|
|
2086
|
+
inputShape: browserNetworkShape,
|
|
2087
|
+
build(ctx) {
|
|
2088
|
+
return safeHandler(async (args) => {
|
|
2089
|
+
const engine = ctx.registry.engineFor(args.session_id);
|
|
2090
|
+
if (!(engine instanceof PlaywrightEngine)) {
|
|
2091
|
+
throw new RolepodMcpError(
|
|
2092
|
+
"unsupported_engine",
|
|
2093
|
+
"network is web-only and requires PlaywrightEngine."
|
|
2094
|
+
);
|
|
2095
|
+
}
|
|
2096
|
+
const requests = engine.getNetwork(args.session_id, {
|
|
2097
|
+
...args.url_pattern !== void 0 ? { urlPattern: args.url_pattern } : {},
|
|
2098
|
+
patternKind: args.pattern_kind,
|
|
2099
|
+
...args.method !== void 0 ? { method: args.method } : {},
|
|
2100
|
+
...args.status_range !== void 0 ? { statusRange: args.status_range } : {},
|
|
2101
|
+
onlyFailed: args.only_failed,
|
|
2102
|
+
clear: args.clear,
|
|
2103
|
+
limit: args.limit
|
|
2104
|
+
});
|
|
2105
|
+
const failed = requests.filter(
|
|
2106
|
+
(r) => !!r.failure || r.status !== void 0 && r.status >= 400
|
|
2107
|
+
).length;
|
|
2108
|
+
return ok({
|
|
2109
|
+
count: requests.length,
|
|
2110
|
+
failed_count: failed,
|
|
2111
|
+
requests,
|
|
2112
|
+
// HAR file lives wherever the session was opened with
|
|
2113
|
+
// `capture.har.path`. We don't echo it here to avoid leaking
|
|
2114
|
+
// filesystem paths into untrusted logs; the verify_ui_flow run
|
|
2115
|
+
// result surfaces it in `evidence_paths.har`.
|
|
2116
|
+
har_recording: args.export_har ? "HAR is written at session close to the path passed via capture.har at open time." : void 0
|
|
2117
|
+
});
|
|
2118
|
+
});
|
|
2119
|
+
}
|
|
2120
|
+
};
|
|
2121
|
+
|
|
1273
2122
|
// src/tools/atomic/browser_open.ts
|
|
1274
2123
|
var browserOpenTool = {
|
|
1275
2124
|
name: ToolNames.browserOpen,
|
|
@@ -1283,6 +2132,34 @@ var browserOpenTool = {
|
|
|
1283
2132
|
}
|
|
1284
2133
|
};
|
|
1285
2134
|
|
|
2135
|
+
// src/tools/atomic/browser_pages.ts
|
|
2136
|
+
var browserPagesTool = {
|
|
2137
|
+
name: ToolNames.browserPages,
|
|
2138
|
+
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.",
|
|
2139
|
+
inputShape: browserPagesShape,
|
|
2140
|
+
build(ctx) {
|
|
2141
|
+
return safeHandler(async (args) => {
|
|
2142
|
+
const engine = ctx.registry.engineFor(args.session_id);
|
|
2143
|
+
if (!(engine instanceof PlaywrightEngine)) {
|
|
2144
|
+
throw new RolepodMcpError(
|
|
2145
|
+
"unsupported_engine",
|
|
2146
|
+
"pages is web-only and requires PlaywrightEngine."
|
|
2147
|
+
);
|
|
2148
|
+
}
|
|
2149
|
+
const raw = engine.listPages(args.session_id);
|
|
2150
|
+
const pages = await Promise.all(
|
|
2151
|
+
raw.map(async (p) => ({
|
|
2152
|
+
index: p.index,
|
|
2153
|
+
url: p.url,
|
|
2154
|
+
title: await p.title_promise.catch(() => ""),
|
|
2155
|
+
active: p.active
|
|
2156
|
+
}))
|
|
2157
|
+
);
|
|
2158
|
+
return ok({ count: pages.length, pages });
|
|
2159
|
+
});
|
|
2160
|
+
}
|
|
2161
|
+
};
|
|
2162
|
+
|
|
1286
2163
|
// src/tools/atomic/browser_screenshot.ts
|
|
1287
2164
|
var browserScreenshotTool = {
|
|
1288
2165
|
name: ToolNames.browserScreenshot,
|
|
@@ -1327,6 +2204,35 @@ var browserScrollTool = {
|
|
|
1327
2204
|
}
|
|
1328
2205
|
};
|
|
1329
2206
|
|
|
2207
|
+
// src/tools/atomic/browser_set_env.ts
|
|
2208
|
+
var browserSetEnvTool = {
|
|
2209
|
+
name: ToolNames.browserSetEnv,
|
|
2210
|
+
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.",
|
|
2211
|
+
inputShape: browserSetEnvShape,
|
|
2212
|
+
build(ctx) {
|
|
2213
|
+
return safeHandler(async (args) => {
|
|
2214
|
+
const engine = ctx.registry.engineFor(args.session_id);
|
|
2215
|
+
if (!(engine instanceof PlaywrightEngine)) {
|
|
2216
|
+
throw new RolepodMcpError(
|
|
2217
|
+
"unsupported_engine",
|
|
2218
|
+
"set_env is web-only and requires PlaywrightEngine."
|
|
2219
|
+
);
|
|
2220
|
+
}
|
|
2221
|
+
await engine.setEnv(args.session_id, {
|
|
2222
|
+
...args.viewport !== void 0 ? { viewport: args.viewport } : {},
|
|
2223
|
+
...args.offline !== void 0 ? { offline: args.offline } : {},
|
|
2224
|
+
...args.geolocation !== void 0 ? { geolocation: args.geolocation } : {},
|
|
2225
|
+
...args.color_scheme !== void 0 ? { colorScheme: args.color_scheme } : {},
|
|
2226
|
+
...args.reduced_motion !== void 0 ? { reducedMotion: args.reduced_motion } : {},
|
|
2227
|
+
...args.extra_headers !== void 0 ? { extraHeaders: args.extra_headers } : {},
|
|
2228
|
+
...args.network_throttle !== void 0 ? { networkThrottle: args.network_throttle } : {},
|
|
2229
|
+
...args.cpu_throttle !== void 0 ? { cpuThrottle: args.cpu_throttle } : {}
|
|
2230
|
+
});
|
|
2231
|
+
return ok({ applied: true });
|
|
2232
|
+
});
|
|
2233
|
+
}
|
|
2234
|
+
};
|
|
2235
|
+
|
|
1330
2236
|
// src/tools/atomic/browser_snapshot.ts
|
|
1331
2237
|
var browserSnapshotTool = {
|
|
1332
2238
|
name: ToolNames.browserSnapshot,
|
|
@@ -1349,6 +2255,26 @@ var browserSnapshotTool = {
|
|
|
1349
2255
|
}
|
|
1350
2256
|
};
|
|
1351
2257
|
|
|
2258
|
+
// src/tools/atomic/browser_switch_page.ts
|
|
2259
|
+
var browserSwitchPageTool = {
|
|
2260
|
+
name: ToolNames.browserSwitchPage,
|
|
2261
|
+
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.",
|
|
2262
|
+
inputShape: browserSwitchPageShape,
|
|
2263
|
+
build(ctx) {
|
|
2264
|
+
return safeHandler(async (args) => {
|
|
2265
|
+
const engine = ctx.registry.engineFor(args.session_id);
|
|
2266
|
+
if (!(engine instanceof PlaywrightEngine)) {
|
|
2267
|
+
throw new RolepodMcpError(
|
|
2268
|
+
"unsupported_engine",
|
|
2269
|
+
"switch_page is web-only and requires PlaywrightEngine."
|
|
2270
|
+
);
|
|
2271
|
+
}
|
|
2272
|
+
await engine.switchPage(args.session_id, args.index);
|
|
2273
|
+
return ok({ active_index: args.index });
|
|
2274
|
+
});
|
|
2275
|
+
}
|
|
2276
|
+
};
|
|
2277
|
+
|
|
1352
2278
|
// src/tools/atomic/browser_type.ts
|
|
1353
2279
|
var browserTypeTool = {
|
|
1354
2280
|
name: ToolNames.browserType,
|
|
@@ -1368,6 +2294,27 @@ var browserTypeTool = {
|
|
|
1368
2294
|
}
|
|
1369
2295
|
};
|
|
1370
2296
|
|
|
2297
|
+
// src/tools/atomic/browser_upload_file.ts
|
|
2298
|
+
var browserUploadFileTool = {
|
|
2299
|
+
name: ToolNames.browserUploadFile,
|
|
2300
|
+
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.",
|
|
2301
|
+
inputShape: browserUploadFileShape,
|
|
2302
|
+
build(ctx) {
|
|
2303
|
+
return safeHandler(async (args) => {
|
|
2304
|
+
const engine = ctx.registry.engineFor(args.session_id);
|
|
2305
|
+
await engine.uploadFile(
|
|
2306
|
+
{
|
|
2307
|
+
id: args.session_id,
|
|
2308
|
+
platform: ctx.registry.platformOf(args.session_id)
|
|
2309
|
+
},
|
|
2310
|
+
args.ref,
|
|
2311
|
+
args.file_path
|
|
2312
|
+
);
|
|
2313
|
+
return ok({ uploaded: true, file_path: args.file_path });
|
|
2314
|
+
});
|
|
2315
|
+
}
|
|
2316
|
+
};
|
|
2317
|
+
|
|
1371
2318
|
// src/tools/atomic/browser_wait_for.ts
|
|
1372
2319
|
var browserWaitForTool = {
|
|
1373
2320
|
name: ToolNames.browserWaitFor,
|
|
@@ -1740,6 +2687,67 @@ function playwrightStepLine(step) {
|
|
|
1740
2687
|
return ` await page.goto(${JSON.stringify(step.url)});`;
|
|
1741
2688
|
case "wait_for":
|
|
1742
2689
|
return ` // wait_for: ${JSON.stringify(step.condition)} \u2014 translate to page.waitForXxx()`;
|
|
2690
|
+
case "hover":
|
|
2691
|
+
return ` await page.getByText(${JSON.stringify(step.query)}, { exact: false }).first().hover();`;
|
|
2692
|
+
case "drag":
|
|
2693
|
+
return [
|
|
2694
|
+
` await page`,
|
|
2695
|
+
` .getByText(${JSON.stringify(step.from_query)}, { exact: false })`,
|
|
2696
|
+
` .first()`,
|
|
2697
|
+
` .dragTo(page.getByText(${JSON.stringify(step.to_query)}, { exact: false }).first());`
|
|
2698
|
+
].join("\n");
|
|
2699
|
+
case "fill_form": {
|
|
2700
|
+
const fields = Array.isArray(step.fields) ? step.fields : [];
|
|
2701
|
+
return fields.map((f) => {
|
|
2702
|
+
const q = JSON.stringify(f.query);
|
|
2703
|
+
if (f.kind === "select") {
|
|
2704
|
+
return ` await page.getByLabel(${q}).selectOption(${JSON.stringify(String(f.value))});`;
|
|
2705
|
+
}
|
|
2706
|
+
if (f.kind === "checkbox" || f.kind === "radio") {
|
|
2707
|
+
const checked = typeof f.value === "boolean" ? f.value : String(f.value) === "true" || String(f.value) === "on";
|
|
2708
|
+
return ` await page.getByLabel(${q}).setChecked(${checked});`;
|
|
2709
|
+
}
|
|
2710
|
+
return ` await page.getByLabel(${q}).fill(${JSON.stringify(String(f.value))});`;
|
|
2711
|
+
}).join("\n");
|
|
2712
|
+
}
|
|
2713
|
+
case "upload":
|
|
2714
|
+
return ` await page.getByLabel(${JSON.stringify(step.query)}).setInputFiles(${JSON.stringify(step.file_path)});`;
|
|
2715
|
+
case "dialog":
|
|
2716
|
+
return [
|
|
2717
|
+
` page.once("dialog", async (dialog) => {`,
|
|
2718
|
+
step.action === "accept" ? ` await dialog.accept();` : step.action === "accept_with_text" ? ` await dialog.accept(${JSON.stringify(step.text ?? "")});` : ` await dialog.dismiss();`,
|
|
2719
|
+
` });`
|
|
2720
|
+
].join("\n");
|
|
2721
|
+
case "set_env": {
|
|
2722
|
+
const lines = [];
|
|
2723
|
+
if (step.viewport && typeof step.viewport === "object") {
|
|
2724
|
+
const v = step.viewport;
|
|
2725
|
+
lines.push(` await page.setViewportSize({ width: ${v.width}, height: ${v.height} });`);
|
|
2726
|
+
}
|
|
2727
|
+
if (step.offline !== void 0) {
|
|
2728
|
+
lines.push(` await page.context().setOffline(${Boolean(step.offline)});`);
|
|
2729
|
+
}
|
|
2730
|
+
if (step.geolocation) {
|
|
2731
|
+
lines.push(` await page.context().setGeolocation(${JSON.stringify(step.geolocation)});`);
|
|
2732
|
+
}
|
|
2733
|
+
if (step.color_scheme || step.reduced_motion) {
|
|
2734
|
+
const opts = {};
|
|
2735
|
+
if (step.color_scheme) opts.colorScheme = step.color_scheme;
|
|
2736
|
+
if (step.reduced_motion) opts.reducedMotion = step.reduced_motion;
|
|
2737
|
+
lines.push(` await page.emulateMedia(${JSON.stringify(opts)});`);
|
|
2738
|
+
}
|
|
2739
|
+
if (step.extra_headers) {
|
|
2740
|
+
lines.push(` await page.context().setExtraHTTPHeaders(${JSON.stringify(step.extra_headers)});`);
|
|
2741
|
+
}
|
|
2742
|
+
if (step.network_throttle || step.cpu_throttle !== void 0) {
|
|
2743
|
+
lines.push(` // network/cpu throttle requires CDP \u2014 see Playwright docs (chromium only)`);
|
|
2744
|
+
}
|
|
2745
|
+
return lines.length > 0 ? lines.join("\n") : ` // set_env: nothing to apply`;
|
|
2746
|
+
}
|
|
2747
|
+
case "switch_page":
|
|
2748
|
+
return ` const allPages = page.context().pages(); /* switch to index ${step.index} */ if (allPages[${step.index}]) await allPages[${step.index}].bringToFront();`;
|
|
2749
|
+
case "evaluate":
|
|
2750
|
+
return ` await page.evaluate(${JSON.stringify(step.script)});`;
|
|
1743
2751
|
default:
|
|
1744
2752
|
return ` // unsupported step kind: ${step.kind}`;
|
|
1745
2753
|
}
|
|
@@ -1754,6 +2762,20 @@ function playwrightExpectLine(exp) {
|
|
|
1754
2762
|
return ` await expect(page).toHaveURL(new RegExp(${JSON.stringify(exp.pattern)}));`;
|
|
1755
2763
|
case "ref_in_state":
|
|
1756
2764
|
return ` // ref_in_state ${JSON.stringify(exp.query)} \u2192 ${String(exp.state)} \u2014 translate as needed`;
|
|
2765
|
+
case "no_console_errors":
|
|
2766
|
+
return [
|
|
2767
|
+
` // no_console_errors \u2014 collect via page.on('console') before the steps, then:`,
|
|
2768
|
+
` // expect(consoleErrors).toEqual([]);`
|
|
2769
|
+
].join("\n");
|
|
2770
|
+
case "no_failed_requests":
|
|
2771
|
+
return [
|
|
2772
|
+
` // no_failed_requests \u2014 collect via page.on('requestfailed'/'response') before the steps, then:`,
|
|
2773
|
+
` // expect(failedRequests).toEqual([]);`
|
|
2774
|
+
].join("\n");
|
|
2775
|
+
case "request_made":
|
|
2776
|
+
return ` await page.waitForRequest(new RegExp(${JSON.stringify(exp.url_pattern)}));`;
|
|
2777
|
+
case "response_status":
|
|
2778
|
+
return ` await page.waitForResponse((r) => new RegExp(${JSON.stringify(exp.url_pattern)}).test(r.url()) && r.status() === ${Number(exp.status)});`;
|
|
1757
2779
|
default:
|
|
1758
2780
|
return ` // unsupported expect kind: ${exp.kind}`;
|
|
1759
2781
|
}
|
|
@@ -1770,6 +2792,56 @@ function seleniumStepLine(step) {
|
|
|
1770
2792
|
return ` driver.get(${JSON.stringify(step.url)})`;
|
|
1771
2793
|
case "wait_for":
|
|
1772
2794
|
return ` # wait_for: ${JSON.stringify(step.condition)} \u2014 translate to WebDriverWait`;
|
|
2795
|
+
case "hover":
|
|
2796
|
+
return [
|
|
2797
|
+
` from selenium.webdriver.common.action_chains import ActionChains`,
|
|
2798
|
+
` target = driver.find_element(By.XPATH, f"//*[contains(text(), \\"${escapePy(String(step.query))}\\")]")`,
|
|
2799
|
+
` ActionChains(driver).move_to_element(target).perform()`
|
|
2800
|
+
].join("\n");
|
|
2801
|
+
case "drag":
|
|
2802
|
+
return [
|
|
2803
|
+
` from selenium.webdriver.common.action_chains import ActionChains`,
|
|
2804
|
+
` src = driver.find_element(By.XPATH, f"//*[contains(text(), \\"${escapePy(String(step.from_query))}\\")]")`,
|
|
2805
|
+
` dst = driver.find_element(By.XPATH, f"//*[contains(text(), \\"${escapePy(String(step.to_query))}\\")]")`,
|
|
2806
|
+
` ActionChains(driver).drag_and_drop(src, dst).perform()`
|
|
2807
|
+
].join("\n");
|
|
2808
|
+
case "fill_form": {
|
|
2809
|
+
const fields = Array.isArray(step.fields) ? step.fields : [];
|
|
2810
|
+
return fields.map((f) => {
|
|
2811
|
+
const q = escapePy(f.query);
|
|
2812
|
+
if (f.kind === "select") {
|
|
2813
|
+
return [
|
|
2814
|
+
` from selenium.webdriver.support.ui import Select`,
|
|
2815
|
+
` Select(driver.find_element(By.XPATH, f"//*[@aria-label=\\"${q}\\"]")).select_by_visible_text(${JSON.stringify(String(f.value))})`
|
|
2816
|
+
].join("\n");
|
|
2817
|
+
}
|
|
2818
|
+
if (f.kind === "checkbox" || f.kind === "radio") {
|
|
2819
|
+
const checked = typeof f.value === "boolean" ? f.value : String(f.value) === "true";
|
|
2820
|
+
return ` el = driver.find_element(By.XPATH, f"//*[@aria-label=\\"${q}\\"]"); el.click() if el.is_selected() != ${checked ? "True" : "False"} else None`;
|
|
2821
|
+
}
|
|
2822
|
+
return ` driver.find_element(By.XPATH, f"//*[@aria-label=\\"${q}\\"]").send_keys(${JSON.stringify(String(f.value))})`;
|
|
2823
|
+
}).join("\n");
|
|
2824
|
+
}
|
|
2825
|
+
case "upload":
|
|
2826
|
+
return ` driver.find_element(By.XPATH, f"//*[@aria-label=\\"${escapePy(String(step.query))}\\"]").send_keys(${JSON.stringify(step.file_path)})`;
|
|
2827
|
+
case "dialog":
|
|
2828
|
+
return [
|
|
2829
|
+
` alert = driver.switch_to.alert`,
|
|
2830
|
+
step.action === "accept" ? ` alert.accept()` : step.action === "accept_with_text" ? ` alert.send_keys(${JSON.stringify(step.text ?? "")}); alert.accept()` : ` alert.dismiss()`
|
|
2831
|
+
].join("\n");
|
|
2832
|
+
case "set_env": {
|
|
2833
|
+
const lines = [];
|
|
2834
|
+
if (step.viewport && typeof step.viewport === "object") {
|
|
2835
|
+
const v = step.viewport;
|
|
2836
|
+
lines.push(` driver.set_window_size(${v.width}, ${v.height})`);
|
|
2837
|
+
}
|
|
2838
|
+
lines.push(` # set_env partially supported in Selenium \u2014 see selenium docs for offline/geolocation/colorScheme via CDP`);
|
|
2839
|
+
return lines.join("\n");
|
|
2840
|
+
}
|
|
2841
|
+
case "switch_page":
|
|
2842
|
+
return ` driver.switch_to.window(driver.window_handles[${step.index}])`;
|
|
2843
|
+
case "evaluate":
|
|
2844
|
+
return ` driver.execute_script(${JSON.stringify(step.script)})`;
|
|
1773
2845
|
default:
|
|
1774
2846
|
return ` # unsupported step kind: ${step.kind}`;
|
|
1775
2847
|
}
|
|
@@ -1784,6 +2856,18 @@ function seleniumExpectLine(exp) {
|
|
|
1784
2856
|
return ` import re; assert re.search(${JSON.stringify(exp.pattern)}, driver.current_url)`;
|
|
1785
2857
|
case "ref_in_state":
|
|
1786
2858
|
return ` # ref_in_state ${JSON.stringify(exp.query)} \u2192 ${String(exp.state)}`;
|
|
2859
|
+
case "no_console_errors":
|
|
2860
|
+
return [
|
|
2861
|
+
` # no_console_errors \u2014 read browser logs via driver.get_log("browser")`,
|
|
2862
|
+
` errors = [l for l in driver.get_log("browser") if l.get("level") == "SEVERE"]`,
|
|
2863
|
+
` assert errors == [], f"console errors: {errors}"`
|
|
2864
|
+
].join("\n");
|
|
2865
|
+
case "no_failed_requests":
|
|
2866
|
+
return ` # no_failed_requests \u2014 selenium has no built-in network capture. Enable selenium-wire or BiDi for this.`;
|
|
2867
|
+
case "request_made":
|
|
2868
|
+
return ` # request_made ${JSON.stringify(exp.url_pattern)} \u2014 use selenium-wire (driver.requests) or BiDi`;
|
|
2869
|
+
case "response_status":
|
|
2870
|
+
return ` # response_status ${JSON.stringify(exp.url_pattern)} == ${Number(exp.status)} \u2014 use selenium-wire (driver.requests) or BiDi`;
|
|
1787
2871
|
default:
|
|
1788
2872
|
return ` # unsupported expect kind: ${exp.kind}`;
|
|
1789
2873
|
}
|
|
@@ -1812,6 +2896,10 @@ function indent(block, n) {
|
|
|
1812
2896
|
return block.split("\n").map((l) => l.length > 0 ? pad + l : l).join("\n");
|
|
1813
2897
|
}
|
|
1814
2898
|
|
|
2899
|
+
// src/tools/composite/verify_ui_flow.ts
|
|
2900
|
+
import { readdir } from "fs/promises";
|
|
2901
|
+
import { resolve as resolvePath2 } from "path";
|
|
2902
|
+
|
|
1815
2903
|
// src/replay/minimize.ts
|
|
1816
2904
|
async function ddmin(input, reproduces) {
|
|
1817
2905
|
let current = [...input];
|
|
@@ -1875,13 +2963,32 @@ var verifyUiFlowTool = {
|
|
|
1875
2963
|
});
|
|
1876
2964
|
}
|
|
1877
2965
|
};
|
|
2966
|
+
function buildCaptureOptions(captures, runDir) {
|
|
2967
|
+
const cap = {};
|
|
2968
|
+
if (captures.has("har")) {
|
|
2969
|
+
cap.har = { path: resolvePath2(runDir, "network.har") };
|
|
2970
|
+
}
|
|
2971
|
+
if (captures.has("video")) {
|
|
2972
|
+
cap.video = { dir: resolvePath2(runDir, "videos") };
|
|
2973
|
+
}
|
|
2974
|
+
if (captures.has("trace")) {
|
|
2975
|
+
cap.trace = { artifactDir: runDir };
|
|
2976
|
+
}
|
|
2977
|
+
return Object.keys(cap).length > 0 ? cap : void 0;
|
|
2978
|
+
}
|
|
1878
2979
|
async function runFlow(ctx, args, steps, runDir, opts) {
|
|
1879
2980
|
const evidence = { screenshots: [] };
|
|
2981
|
+
const captures = new Set(args.capture ?? ["screenshot"]);
|
|
1880
2982
|
let passed = false;
|
|
1881
2983
|
let failedAtStep;
|
|
1882
2984
|
let failureReason;
|
|
1883
2985
|
let finalSnapshot;
|
|
1884
|
-
const
|
|
2986
|
+
const openOpts = { ...args.open };
|
|
2987
|
+
const captureCfg = buildCaptureOptions(captures, runDir);
|
|
2988
|
+
if (captureCfg) {
|
|
2989
|
+
openOpts.capture = captureCfg;
|
|
2990
|
+
}
|
|
2991
|
+
const session = await ctx.registry.open(openOpts);
|
|
1885
2992
|
const engine = ctx.registry.engineFor(session.id);
|
|
1886
2993
|
const sessionHandle = { id: session.id, platform: session.platform };
|
|
1887
2994
|
try {
|
|
@@ -1900,7 +3007,7 @@ async function runFlow(ctx, args, steps, runDir, opts) {
|
|
|
1900
3007
|
const failures = [];
|
|
1901
3008
|
for (let i = 0; i < args.expect.length; i++) {
|
|
1902
3009
|
const expectation = args.expect[i];
|
|
1903
|
-
if (!evaluateExpect(expectation, finalSnapshot)) {
|
|
3010
|
+
if (!evaluateExpect(expectation, finalSnapshot, engine, session.id)) {
|
|
1904
3011
|
failures.push(`expect[${i}] ${describeExpect(expectation)}`);
|
|
1905
3012
|
}
|
|
1906
3013
|
}
|
|
@@ -1914,8 +3021,7 @@ async function runFlow(ctx, args, steps, runDir, opts) {
|
|
|
1914
3021
|
passed = false;
|
|
1915
3022
|
} finally {
|
|
1916
3023
|
if (opts.captureEvidence) {
|
|
1917
|
-
|
|
1918
|
-
if (wantScreenshot) {
|
|
3024
|
+
if (captures.has("screenshot")) {
|
|
1919
3025
|
try {
|
|
1920
3026
|
const buf = await engine.screenshot(sessionHandle, true);
|
|
1921
3027
|
const p = await ctx.store.writeScreenshot(runDir, buf, "final");
|
|
@@ -1924,6 +3030,37 @@ async function runFlow(ctx, args, steps, runDir, opts) {
|
|
|
1924
3030
|
failureReason ??= `screenshot capture failed: ${describeError(err)}`;
|
|
1925
3031
|
}
|
|
1926
3032
|
}
|
|
3033
|
+
if (captures.has("console") && engine instanceof PlaywrightEngine) {
|
|
3034
|
+
try {
|
|
3035
|
+
const messages = engine.peekBuffers(session.id).console;
|
|
3036
|
+
evidence.console = await ctx.store.writeReport(
|
|
3037
|
+
runDir,
|
|
3038
|
+
"console.json",
|
|
3039
|
+
JSON.stringify(
|
|
3040
|
+
{
|
|
3041
|
+
count: messages.length,
|
|
3042
|
+
by_level: countByLevel(messages),
|
|
3043
|
+
messages
|
|
3044
|
+
},
|
|
3045
|
+
null,
|
|
3046
|
+
2
|
|
3047
|
+
)
|
|
3048
|
+
);
|
|
3049
|
+
} catch (err) {
|
|
3050
|
+
failureReason ??= `console capture failed: ${describeError(err)}`;
|
|
3051
|
+
}
|
|
3052
|
+
}
|
|
3053
|
+
if (captures.has("a11y_tree") && finalSnapshot) {
|
|
3054
|
+
try {
|
|
3055
|
+
evidence.a11y_tree = await ctx.store.writeReport(
|
|
3056
|
+
runDir,
|
|
3057
|
+
"a11y_tree.json",
|
|
3058
|
+
JSON.stringify(finalSnapshot, null, 2)
|
|
3059
|
+
);
|
|
3060
|
+
} catch (err) {
|
|
3061
|
+
failureReason ??= `a11y_tree capture failed: ${describeError(err)}`;
|
|
3062
|
+
}
|
|
3063
|
+
}
|
|
1927
3064
|
try {
|
|
1928
3065
|
evidence.replay_bundle = await ctx.store.writeReplayBundle(
|
|
1929
3066
|
runDir,
|
|
@@ -1944,12 +3081,32 @@ async function runFlow(ctx, args, steps, runDir, opts) {
|
|
|
1944
3081
|
await ctx.registry.close(sessionHandle).catch(() => void 0);
|
|
1945
3082
|
}
|
|
1946
3083
|
}
|
|
3084
|
+
if (opts.captureEvidence) {
|
|
3085
|
+
if (captureCfg?.har) evidence.har = captureCfg.har.path;
|
|
3086
|
+
if (captureCfg?.trace) {
|
|
3087
|
+
evidence.trace = resolvePath2(captureCfg.trace.artifactDir, "trace.zip");
|
|
3088
|
+
}
|
|
3089
|
+
if (captureCfg?.video) {
|
|
3090
|
+
try {
|
|
3091
|
+
const files = await readdir(captureCfg.video.dir).catch(() => []);
|
|
3092
|
+
evidence.video = files.filter((f) => f.endsWith(".webm")).map((f) => resolvePath2(captureCfg.video.dir, f));
|
|
3093
|
+
} catch {
|
|
3094
|
+
}
|
|
3095
|
+
}
|
|
3096
|
+
}
|
|
1947
3097
|
const out = { passed, evidence };
|
|
1948
3098
|
if (failedAtStep !== void 0) out.failedAtStep = failedAtStep;
|
|
1949
3099
|
if (failureReason !== void 0) out.failureReason = failureReason;
|
|
1950
3100
|
if (finalSnapshot) out.finalUrl = finalSnapshot.url_or_screen;
|
|
1951
3101
|
return out;
|
|
1952
3102
|
}
|
|
3103
|
+
function countByLevel(messages) {
|
|
3104
|
+
const counts = {};
|
|
3105
|
+
for (const m of messages) {
|
|
3106
|
+
counts[m.level] = (counts[m.level] ?? 0) + 1;
|
|
3107
|
+
}
|
|
3108
|
+
return counts;
|
|
3109
|
+
}
|
|
1953
3110
|
async function minimize(ctx, args, initialSteps, runDir) {
|
|
1954
3111
|
const tagged = initialSteps.map((step, origIndex) => ({ step, origIndex }));
|
|
1955
3112
|
let attempts = 0;
|
|
@@ -2013,9 +3170,84 @@ async function runStep(engine, session, step, snap) {
|
|
|
2013
3170
|
case "navigate":
|
|
2014
3171
|
await engine.navigate(session, step.url);
|
|
2015
3172
|
return;
|
|
3173
|
+
case "hover": {
|
|
3174
|
+
const ref = findRefByQuery(snap.tree, step.query);
|
|
3175
|
+
if (!ref) throw missingQuery(step.query);
|
|
3176
|
+
await engine.hover(session, ref);
|
|
3177
|
+
return;
|
|
3178
|
+
}
|
|
3179
|
+
case "drag": {
|
|
3180
|
+
const fromRef = findRefByQuery(snap.tree, step.from_query);
|
|
3181
|
+
if (!fromRef) throw missingQuery(step.from_query);
|
|
3182
|
+
const toRef = findRefByQuery(snap.tree, step.to_query);
|
|
3183
|
+
if (!toRef) throw missingQuery(step.to_query);
|
|
3184
|
+
await engine.drag(session, fromRef, toRef);
|
|
3185
|
+
return;
|
|
3186
|
+
}
|
|
3187
|
+
case "fill_form": {
|
|
3188
|
+
const resolved = step.fields.map((f) => {
|
|
3189
|
+
const ref = findRefByQuery(snap.tree, f.query);
|
|
3190
|
+
if (!ref) throw missingQuery(f.query);
|
|
3191
|
+
return f.kind !== void 0 ? { ref, value: f.value, kind: f.kind } : { ref, value: f.value };
|
|
3192
|
+
});
|
|
3193
|
+
await engine.fillForm(session, resolved);
|
|
3194
|
+
return;
|
|
3195
|
+
}
|
|
3196
|
+
case "upload": {
|
|
3197
|
+
const ref = findRefByQuery(snap.tree, step.query);
|
|
3198
|
+
if (!ref) throw missingQuery(step.query);
|
|
3199
|
+
await engine.uploadFile(session, ref, step.file_path);
|
|
3200
|
+
return;
|
|
3201
|
+
}
|
|
3202
|
+
case "dialog": {
|
|
3203
|
+
requirePlaywright(engine, "dialog");
|
|
3204
|
+
void engine.handleDialog(session.id, {
|
|
3205
|
+
action: step.action,
|
|
3206
|
+
...step.text !== void 0 ? { text: step.text } : {}
|
|
3207
|
+
}).catch(() => void 0);
|
|
3208
|
+
return;
|
|
3209
|
+
}
|
|
3210
|
+
case "set_env": {
|
|
3211
|
+
requirePlaywright(engine, "set_env");
|
|
3212
|
+
await engine.setEnv(session.id, {
|
|
3213
|
+
...step.viewport !== void 0 ? { viewport: step.viewport } : {},
|
|
3214
|
+
...step.offline !== void 0 ? { offline: step.offline } : {},
|
|
3215
|
+
...step.geolocation !== void 0 ? { geolocation: step.geolocation } : {},
|
|
3216
|
+
...step.color_scheme !== void 0 ? { colorScheme: step.color_scheme } : {},
|
|
3217
|
+
...step.reduced_motion !== void 0 ? { reducedMotion: step.reduced_motion } : {},
|
|
3218
|
+
...step.extra_headers !== void 0 ? { extraHeaders: step.extra_headers } : {},
|
|
3219
|
+
...step.network_throttle !== void 0 ? { networkThrottle: step.network_throttle } : {},
|
|
3220
|
+
...step.cpu_throttle !== void 0 ? { cpuThrottle: step.cpu_throttle } : {}
|
|
3221
|
+
});
|
|
3222
|
+
return;
|
|
3223
|
+
}
|
|
3224
|
+
case "switch_page": {
|
|
3225
|
+
requirePlaywright(engine, "switch_page");
|
|
3226
|
+
await engine.switchPage(session.id, step.index);
|
|
3227
|
+
return;
|
|
3228
|
+
}
|
|
3229
|
+
case "evaluate": {
|
|
3230
|
+
requirePlaywright(engine, "evaluate");
|
|
3231
|
+
if (process.env.ROLEPOD_ALLOW_EVAL !== "1") {
|
|
3232
|
+
throw new RolepodMcpError(
|
|
3233
|
+
"engine_error",
|
|
3234
|
+
"verify_ui_flow step kind 'evaluate' is disabled. Restart the rolepod-uiproof MCP server with ROLEPOD_ALLOW_EVAL=1 to enable."
|
|
3235
|
+
);
|
|
3236
|
+
}
|
|
3237
|
+
await engine.evaluate(session.id, step.script);
|
|
3238
|
+
return;
|
|
3239
|
+
}
|
|
3240
|
+
}
|
|
3241
|
+
}
|
|
3242
|
+
function requirePlaywright(engine, stepKind) {
|
|
3243
|
+
if (!(engine instanceof PlaywrightEngine)) {
|
|
3244
|
+
throw new RolepodMcpError(
|
|
3245
|
+
"unsupported_engine",
|
|
3246
|
+
`verify_ui_flow step kind "${stepKind}" is web-only and requires PlaywrightEngine.`
|
|
3247
|
+
);
|
|
2016
3248
|
}
|
|
2017
3249
|
}
|
|
2018
|
-
function evaluateExpect(exp, snap) {
|
|
3250
|
+
function evaluateExpect(exp, snap, engine, sessionId) {
|
|
2019
3251
|
switch (exp.kind) {
|
|
2020
3252
|
case "text_visible":
|
|
2021
3253
|
return treeHasText(snap.tree, exp.text);
|
|
@@ -2035,6 +3267,49 @@ function evaluateExpect(exp, snap) {
|
|
|
2035
3267
|
return node.state?.focused === true;
|
|
2036
3268
|
}
|
|
2037
3269
|
}
|
|
3270
|
+
case "no_console_errors": {
|
|
3271
|
+
if (!(engine instanceof PlaywrightEngine)) return true;
|
|
3272
|
+
const msgs = engine.peekBuffers(sessionId).console.filter(
|
|
3273
|
+
(m) => m.level === "error"
|
|
3274
|
+
);
|
|
3275
|
+
const excludes = exp.exclude_patterns ?? [];
|
|
3276
|
+
const remaining = msgs.filter(
|
|
3277
|
+
(m) => !excludes.some((p) => m.text.includes(p))
|
|
3278
|
+
);
|
|
3279
|
+
return remaining.length === 0;
|
|
3280
|
+
}
|
|
3281
|
+
case "no_failed_requests": {
|
|
3282
|
+
if (!(engine instanceof PlaywrightEngine)) return true;
|
|
3283
|
+
const reqs = engine.peekBuffers(sessionId).network.filter((r) => {
|
|
3284
|
+
if (r.failure) return true;
|
|
3285
|
+
if (r.status === void 0) return false;
|
|
3286
|
+
if (exp.allow_4xx) return r.status >= 500;
|
|
3287
|
+
return r.status >= 400;
|
|
3288
|
+
});
|
|
3289
|
+
const excludes = exp.exclude_patterns ?? [];
|
|
3290
|
+
const remaining = reqs.filter(
|
|
3291
|
+
(r) => !excludes.some((p) => r.url.includes(p))
|
|
3292
|
+
);
|
|
3293
|
+
return remaining.length === 0;
|
|
3294
|
+
}
|
|
3295
|
+
case "request_made": {
|
|
3296
|
+
if (!(engine instanceof PlaywrightEngine)) return false;
|
|
3297
|
+
const re = new RegExp(exp.url_pattern);
|
|
3298
|
+
const wantMethod = exp.method?.toUpperCase();
|
|
3299
|
+
const matches = engine.peekBuffers(sessionId).network.filter((r) => {
|
|
3300
|
+
if (!re.test(r.url)) return false;
|
|
3301
|
+
if (wantMethod && r.method.toUpperCase() !== wantMethod) return false;
|
|
3302
|
+
return true;
|
|
3303
|
+
});
|
|
3304
|
+
const min = exp.min_count ?? 1;
|
|
3305
|
+
return matches.length >= min;
|
|
3306
|
+
}
|
|
3307
|
+
case "response_status": {
|
|
3308
|
+
if (!(engine instanceof PlaywrightEngine)) return false;
|
|
3309
|
+
const re = new RegExp(exp.url_pattern);
|
|
3310
|
+
const match = engine.peekBuffers(sessionId).network.find((r) => re.test(r.url) && r.status === exp.status);
|
|
3311
|
+
return match !== void 0;
|
|
3312
|
+
}
|
|
2038
3313
|
}
|
|
2039
3314
|
}
|
|
2040
3315
|
function describeExpect(exp) {
|
|
@@ -2047,6 +3322,14 @@ function describeExpect(exp) {
|
|
|
2047
3322
|
return `url_matches /${exp.pattern}/`;
|
|
2048
3323
|
case "ref_in_state":
|
|
2049
3324
|
return `ref_in_state "${exp.query}" \u2192 ${exp.state}`;
|
|
3325
|
+
case "no_console_errors":
|
|
3326
|
+
return "no_console_errors";
|
|
3327
|
+
case "no_failed_requests":
|
|
3328
|
+
return "no_failed_requests";
|
|
3329
|
+
case "request_made":
|
|
3330
|
+
return `request_made ${exp.method ?? ""} ${exp.url_pattern}`.trim();
|
|
3331
|
+
case "response_status":
|
|
3332
|
+
return `response_status ${exp.url_pattern} = ${exp.status}`;
|
|
2050
3333
|
}
|
|
2051
3334
|
}
|
|
2052
3335
|
function missingQuery(query) {
|
|
@@ -2340,12 +3623,119 @@ var toolMetadata = {
|
|
|
2340
3623
|
readOnlyHint: true,
|
|
2341
3624
|
openWorldHint: true
|
|
2342
3625
|
}
|
|
3626
|
+
},
|
|
3627
|
+
// ---------- v0.5 atomic additions ----------
|
|
3628
|
+
[ToolNames.browserHover]: {
|
|
3629
|
+
title: "Hover Element",
|
|
3630
|
+
annotations: {
|
|
3631
|
+
title: "Hover Element",
|
|
3632
|
+
readOnlyHint: false,
|
|
3633
|
+
destructiveHint: false,
|
|
3634
|
+
idempotentHint: true,
|
|
3635
|
+
openWorldHint: true
|
|
3636
|
+
}
|
|
3637
|
+
},
|
|
3638
|
+
[ToolNames.browserDrag]: {
|
|
3639
|
+
title: "Drag Element",
|
|
3640
|
+
annotations: {
|
|
3641
|
+
title: "Drag Element",
|
|
3642
|
+
readOnlyHint: false,
|
|
3643
|
+
destructiveHint: true,
|
|
3644
|
+
idempotentHint: false,
|
|
3645
|
+
openWorldHint: true
|
|
3646
|
+
}
|
|
3647
|
+
},
|
|
3648
|
+
[ToolNames.browserFillForm]: {
|
|
3649
|
+
title: "Fill Form (Batch)",
|
|
3650
|
+
annotations: {
|
|
3651
|
+
title: "Fill Form (Batch)",
|
|
3652
|
+
readOnlyHint: false,
|
|
3653
|
+
destructiveHint: true,
|
|
3654
|
+
idempotentHint: false,
|
|
3655
|
+
openWorldHint: true
|
|
3656
|
+
}
|
|
3657
|
+
},
|
|
3658
|
+
[ToolNames.browserUploadFile]: {
|
|
3659
|
+
title: "Upload File",
|
|
3660
|
+
annotations: {
|
|
3661
|
+
title: "Upload File",
|
|
3662
|
+
readOnlyHint: false,
|
|
3663
|
+
destructiveHint: true,
|
|
3664
|
+
idempotentHint: false,
|
|
3665
|
+
openWorldHint: true
|
|
3666
|
+
}
|
|
3667
|
+
},
|
|
3668
|
+
[ToolNames.browserHandleDialog]: {
|
|
3669
|
+
title: "Pre-arm Dialog Handler",
|
|
3670
|
+
annotations: {
|
|
3671
|
+
title: "Pre-arm Dialog Handler",
|
|
3672
|
+
readOnlyHint: false,
|
|
3673
|
+
destructiveHint: true,
|
|
3674
|
+
idempotentHint: false,
|
|
3675
|
+
openWorldHint: false
|
|
3676
|
+
}
|
|
3677
|
+
},
|
|
3678
|
+
[ToolNames.browserConsole]: {
|
|
3679
|
+
title: "Inspect Console Logs",
|
|
3680
|
+
annotations: {
|
|
3681
|
+
title: "Inspect Console Logs",
|
|
3682
|
+
readOnlyHint: true,
|
|
3683
|
+
openWorldHint: false
|
|
3684
|
+
}
|
|
3685
|
+
},
|
|
3686
|
+
[ToolNames.browserNetwork]: {
|
|
3687
|
+
title: "Inspect Network Requests",
|
|
3688
|
+
annotations: {
|
|
3689
|
+
title: "Inspect Network Requests",
|
|
3690
|
+
readOnlyHint: true,
|
|
3691
|
+
openWorldHint: false
|
|
3692
|
+
}
|
|
3693
|
+
},
|
|
3694
|
+
[ToolNames.browserSetEnv]: {
|
|
3695
|
+
title: "Set Browser Environment",
|
|
3696
|
+
annotations: {
|
|
3697
|
+
title: "Set Browser Environment",
|
|
3698
|
+
readOnlyHint: false,
|
|
3699
|
+
destructiveHint: true,
|
|
3700
|
+
idempotentHint: true,
|
|
3701
|
+
openWorldHint: false
|
|
3702
|
+
}
|
|
3703
|
+
},
|
|
3704
|
+
[ToolNames.browserEvaluate]: {
|
|
3705
|
+
title: "Evaluate JavaScript (gated; arbitrary code execution)",
|
|
3706
|
+
annotations: {
|
|
3707
|
+
title: "Evaluate JavaScript",
|
|
3708
|
+
// Arbitrary code execution in the page context. Gated by
|
|
3709
|
+
// ROLEPOD_ALLOW_EVAL=1 server-side. Always treat as destructive.
|
|
3710
|
+
readOnlyHint: false,
|
|
3711
|
+
destructiveHint: true,
|
|
3712
|
+
idempotentHint: false,
|
|
3713
|
+
openWorldHint: true
|
|
3714
|
+
}
|
|
3715
|
+
},
|
|
3716
|
+
[ToolNames.browserPages]: {
|
|
3717
|
+
title: "List Open Pages",
|
|
3718
|
+
annotations: {
|
|
3719
|
+
title: "List Open Pages",
|
|
3720
|
+
readOnlyHint: true,
|
|
3721
|
+
openWorldHint: false
|
|
3722
|
+
}
|
|
3723
|
+
},
|
|
3724
|
+
[ToolNames.browserSwitchPage]: {
|
|
3725
|
+
title: "Switch Active Page",
|
|
3726
|
+
annotations: {
|
|
3727
|
+
title: "Switch Active Page",
|
|
3728
|
+
readOnlyHint: false,
|
|
3729
|
+
destructiveHint: false,
|
|
3730
|
+
idempotentHint: true,
|
|
3731
|
+
openWorldHint: false
|
|
3732
|
+
}
|
|
2343
3733
|
}
|
|
2344
3734
|
};
|
|
2345
3735
|
|
|
2346
3736
|
// src/server.ts
|
|
2347
3737
|
var SERVER_NAME = "rolepod-uiproof";
|
|
2348
|
-
var SERVER_VERSION = "0.
|
|
3738
|
+
var SERVER_VERSION = "0.5.0";
|
|
2349
3739
|
function buildServer(opts = {}) {
|
|
2350
3740
|
const webEngine = createWebEngine();
|
|
2351
3741
|
const registry = new SessionRegistry({ idleTimeoutMs: opts.idleTimeoutMs });
|
|
@@ -2362,7 +3752,7 @@ function buildServer(opts = {}) {
|
|
|
2362
3752
|
version: SERVER_VERSION
|
|
2363
3753
|
});
|
|
2364
3754
|
const tools = [
|
|
2365
|
-
// atomic
|
|
3755
|
+
// atomic (v0.1-v0.4)
|
|
2366
3756
|
browserOpenTool,
|
|
2367
3757
|
browserCloseTool,
|
|
2368
3758
|
browserSnapshotTool,
|
|
@@ -2373,6 +3763,18 @@ function buildServer(opts = {}) {
|
|
|
2373
3763
|
browserWaitForTool,
|
|
2374
3764
|
browserScreenshotTool,
|
|
2375
3765
|
browserNavigateTool,
|
|
3766
|
+
// atomic (v0.5)
|
|
3767
|
+
browserHoverTool,
|
|
3768
|
+
browserDragTool,
|
|
3769
|
+
browserFillFormTool,
|
|
3770
|
+
browserUploadFileTool,
|
|
3771
|
+
browserHandleDialogTool,
|
|
3772
|
+
browserConsoleTool,
|
|
3773
|
+
browserNetworkTool,
|
|
3774
|
+
browserSetEnvTool,
|
|
3775
|
+
browserEvaluateTool,
|
|
3776
|
+
browserPagesTool,
|
|
3777
|
+
browserSwitchPageTool,
|
|
2376
3778
|
// composite
|
|
2377
3779
|
verifyUiFlowTool,
|
|
2378
3780
|
auditA11yTool,
|
|
@@ -2426,20 +3828,42 @@ export {
|
|
|
2426
3828
|
browserClickShape,
|
|
2427
3829
|
browserCloseSchema,
|
|
2428
3830
|
browserCloseShape,
|
|
3831
|
+
browserConsoleSchema,
|
|
3832
|
+
browserConsoleShape,
|
|
3833
|
+
browserDragSchema,
|
|
3834
|
+
browserDragShape,
|
|
3835
|
+
browserEvaluateSchema,
|
|
3836
|
+
browserEvaluateShape,
|
|
3837
|
+
browserFillFormSchema,
|
|
3838
|
+
browserFillFormShape,
|
|
3839
|
+
browserHandleDialogSchema,
|
|
3840
|
+
browserHandleDialogShape,
|
|
3841
|
+
browserHoverSchema,
|
|
3842
|
+
browserHoverShape,
|
|
2429
3843
|
browserKeySchema,
|
|
2430
3844
|
browserKeyShape,
|
|
2431
3845
|
browserNavigateSchema,
|
|
2432
3846
|
browserNavigateShape,
|
|
3847
|
+
browserNetworkSchema,
|
|
3848
|
+
browserNetworkShape,
|
|
2433
3849
|
browserOpenSchema,
|
|
2434
3850
|
browserOpenShape,
|
|
3851
|
+
browserPagesSchema,
|
|
3852
|
+
browserPagesShape,
|
|
2435
3853
|
browserScreenshotSchema,
|
|
2436
3854
|
browserScreenshotShape,
|
|
2437
3855
|
browserScrollSchema,
|
|
2438
3856
|
browserScrollShape,
|
|
3857
|
+
browserSetEnvSchema,
|
|
3858
|
+
browserSetEnvShape,
|
|
2439
3859
|
browserSnapshotSchema,
|
|
2440
3860
|
browserSnapshotShape,
|
|
3861
|
+
browserSwitchPageSchema,
|
|
3862
|
+
browserSwitchPageShape,
|
|
2441
3863
|
browserTypeSchema,
|
|
2442
3864
|
browserTypeShape,
|
|
3865
|
+
browserUploadFileSchema,
|
|
3866
|
+
browserUploadFileShape,
|
|
2443
3867
|
browserWaitForSchema,
|
|
2444
3868
|
browserWaitForShape,
|
|
2445
3869
|
buildServer,
|