@midscene/web 1.5.8 → 1.6.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/dist/es/bridge-mode/io-client.mjs +1 -1
- package/dist/es/bridge-mode/io-server.mjs +2 -2
- package/dist/es/bridge-mode/page-browser-side.mjs +1 -1
- package/dist/es/cdp-proxy-constants.mjs +7 -0
- package/dist/es/cdp-proxy-constants.mjs.map +1 -0
- package/dist/es/cdp-proxy.mjs +88 -0
- package/dist/es/cdp-proxy.mjs.map +1 -0
- package/dist/es/cli.mjs +1 -1
- package/dist/es/mcp-server.mjs +1 -1
- package/dist/es/mcp-tools-cdp.mjs +112 -9
- package/dist/es/mcp-tools-cdp.mjs.map +1 -1
- package/dist/es/playwright/reporter/index.mjs +2 -1
- package/dist/es/playwright/reporter/index.mjs.map +1 -1
- package/dist/lib/bridge-mode/io-client.js +1 -1
- package/dist/lib/bridge-mode/io-server.js +2 -2
- package/dist/lib/bridge-mode/page-browser-side.js +1 -1
- package/dist/lib/cdp-proxy-constants.js +44 -0
- package/dist/lib/cdp-proxy-constants.js.map +1 -0
- package/dist/lib/cdp-proxy.js +116 -0
- package/dist/lib/cdp-proxy.js.map +1 -0
- package/dist/lib/cli.js +1 -1
- package/dist/lib/mcp-server.js +1 -1
- package/dist/lib/mcp-tools-cdp.js +112 -9
- package/dist/lib/mcp-tools-cdp.js.map +1 -1
- package/dist/lib/playwright/reporter/index.js +2 -1
- package/dist/lib/playwright/reporter/index.js.map +1 -1
- package/dist/types/cdp-proxy-constants.d.ts +2 -0
- package/dist/types/cdp-proxy.d.ts +21 -0
- package/dist/types/mcp-tools-cdp.d.ts +3 -0
- package/package.json +6 -4
|
@@ -86,7 +86,7 @@ class BridgeServer {
|
|
|
86
86
|
logMsg('one client connected');
|
|
87
87
|
this.socket = socket;
|
|
88
88
|
const clientVersion = socket.handshake.query.version;
|
|
89
|
-
logMsg(`Bridge connected, cli-side version v1.
|
|
89
|
+
logMsg(`Bridge connected, cli-side version v1.6.0, browser-side version v${clientVersion}`);
|
|
90
90
|
socket.on(BridgeEvent.CallResponse, (params)=>{
|
|
91
91
|
const id = params.id;
|
|
92
92
|
const response = params.response;
|
|
@@ -110,7 +110,7 @@ class BridgeServer {
|
|
|
110
110
|
setTimeout(()=>{
|
|
111
111
|
this.onConnect?.();
|
|
112
112
|
const payload = {
|
|
113
|
-
version: "1.
|
|
113
|
+
version: "1.6.0"
|
|
114
114
|
};
|
|
115
115
|
socket.emit(BridgeEvent.Connected, payload);
|
|
116
116
|
Promise.resolve().then(()=>{
|
|
@@ -65,7 +65,7 @@ class ExtensionBridgePageBrowserSide extends page {
|
|
|
65
65
|
throw new Error('Connection denied by user');
|
|
66
66
|
}
|
|
67
67
|
}
|
|
68
|
-
this.onLogMessage(`Bridge connected, cli-side version v${this.bridgeClient.serverVersion}, browser-side version v1.
|
|
68
|
+
this.onLogMessage(`Bridge connected, cli-side version v${this.bridgeClient.serverVersion}, browser-side version v1.6.0`, 'log');
|
|
69
69
|
}
|
|
70
70
|
async connect() {
|
|
71
71
|
return await this.setupBridgeClient();
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { tmpdir } from "node:os";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
const PROXY_ENDPOINT_FILE = join(tmpdir(), 'midscene-cdp-proxy-endpoint');
|
|
4
|
+
const PROXY_PID_FILE = join(tmpdir(), 'midscene-cdp-proxy-pid');
|
|
5
|
+
export { PROXY_ENDPOINT_FILE, PROXY_PID_FILE };
|
|
6
|
+
|
|
7
|
+
//# sourceMappingURL=cdp-proxy-constants.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cdp-proxy-constants.mjs","sources":["../../src/cdp-proxy-constants.ts"],"sourcesContent":["/**\n * Shared constants for CDP proxy discovery between cdp-proxy.ts and mcp-tools-cdp.ts.\n */\nimport { tmpdir } from 'node:os';\nimport { join } from 'node:path';\n\nexport const PROXY_ENDPOINT_FILE = join(\n tmpdir(),\n 'midscene-cdp-proxy-endpoint',\n);\nexport const PROXY_PID_FILE = join(tmpdir(), 'midscene-cdp-proxy-pid');\n"],"names":["PROXY_ENDPOINT_FILE","join","tmpdir","PROXY_PID_FILE"],"mappings":";;AAMO,MAAMA,sBAAsBC,KACjCC,UACA;AAEK,MAAMC,iBAAiBF,KAAKC,UAAU"}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { existsSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { createServer } from "node:http";
|
|
3
|
+
import ws, { WebSocketServer } from "ws";
|
|
4
|
+
import { PROXY_ENDPOINT_FILE, PROXY_PID_FILE } from "./cdp-proxy-constants.mjs";
|
|
5
|
+
const IDLE_TIMEOUT_MS = 300000;
|
|
6
|
+
const chromeEndpoint = process.argv[2];
|
|
7
|
+
if (!chromeEndpoint) {
|
|
8
|
+
process.stderr.write('Usage: node cdp-proxy.js <chrome-ws-endpoint>\n');
|
|
9
|
+
process.exit(1);
|
|
10
|
+
}
|
|
11
|
+
function cleanupIfOwned() {
|
|
12
|
+
try {
|
|
13
|
+
if (existsSync(PROXY_PID_FILE)) {
|
|
14
|
+
const pid = Number(readFileSync(PROXY_PID_FILE, 'utf-8').trim());
|
|
15
|
+
if (pid !== process.pid) return;
|
|
16
|
+
}
|
|
17
|
+
} catch {}
|
|
18
|
+
try {
|
|
19
|
+
if (existsSync(PROXY_ENDPOINT_FILE)) unlinkSync(PROXY_ENDPOINT_FILE);
|
|
20
|
+
} catch {}
|
|
21
|
+
try {
|
|
22
|
+
if (existsSync(PROXY_PID_FILE)) unlinkSync(PROXY_PID_FILE);
|
|
23
|
+
} catch {}
|
|
24
|
+
}
|
|
25
|
+
function shutdown(reason) {
|
|
26
|
+
process.stderr.write(`[cdp-proxy] shutting down: ${reason}\n`);
|
|
27
|
+
cleanupIfOwned();
|
|
28
|
+
process.exit(0);
|
|
29
|
+
}
|
|
30
|
+
process.on('SIGTERM', ()=>shutdown('SIGTERM'));
|
|
31
|
+
process.on('SIGINT', ()=>shutdown('SIGINT'));
|
|
32
|
+
process.on('uncaughtException', (e)=>shutdown(`uncaught: ${e.message}`));
|
|
33
|
+
let idleTimer = null;
|
|
34
|
+
function resetIdleTimer() {
|
|
35
|
+
if (idleTimer) clearTimeout(idleTimer);
|
|
36
|
+
idleTimer = setTimeout(()=>shutdown('idle timeout (5min)'), IDLE_TIMEOUT_MS);
|
|
37
|
+
}
|
|
38
|
+
resetIdleTimer();
|
|
39
|
+
const upstream = new ws(chromeEndpoint);
|
|
40
|
+
const clients = new Set();
|
|
41
|
+
upstream.on('error', (err)=>shutdown(`upstream error: ${err.message}`));
|
|
42
|
+
upstream.on('close', ()=>shutdown('upstream closed'));
|
|
43
|
+
upstream.on('message', (data, isBinary)=>{
|
|
44
|
+
resetIdleTimer();
|
|
45
|
+
for (const client of clients)if (client.readyState === ws.OPEN) client.send(data, {
|
|
46
|
+
binary: isBinary
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
const httpServer = createServer((_req, res)=>{
|
|
50
|
+
res.writeHead(404);
|
|
51
|
+
res.end();
|
|
52
|
+
});
|
|
53
|
+
const wss = new WebSocketServer({
|
|
54
|
+
server: httpServer
|
|
55
|
+
});
|
|
56
|
+
wss.on('connection', (clientWs)=>{
|
|
57
|
+
clients.add(clientWs);
|
|
58
|
+
resetIdleTimer();
|
|
59
|
+
clientWs.on('message', (data, isBinary)=>{
|
|
60
|
+
resetIdleTimer();
|
|
61
|
+
if (upstream.readyState === ws.OPEN) upstream.send(data, {
|
|
62
|
+
binary: isBinary
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
clientWs.on('close', ()=>clients.delete(clientWs));
|
|
66
|
+
clientWs.on('error', ()=>clients.delete(clientWs));
|
|
67
|
+
});
|
|
68
|
+
upstream.on('open', ()=>{
|
|
69
|
+
if (existsSync(PROXY_PID_FILE)) try {
|
|
70
|
+
const existingPid = Number(readFileSync(PROXY_PID_FILE, 'utf-8').trim());
|
|
71
|
+
if (existingPid !== process.pid) try {
|
|
72
|
+
process.kill(existingPid, 0);
|
|
73
|
+
process.exit(0);
|
|
74
|
+
} catch {}
|
|
75
|
+
} catch {}
|
|
76
|
+
httpServer.listen(0, '127.0.0.1', ()=>{
|
|
77
|
+
const addr = httpServer.address();
|
|
78
|
+
if (!addr || 'string' == typeof addr) return void shutdown('failed to get server address');
|
|
79
|
+
const proxyEndpoint = `ws://127.0.0.1:${addr.port}/devtools/browser`;
|
|
80
|
+
writeFileSync(PROXY_ENDPOINT_FILE, proxyEndpoint);
|
|
81
|
+
writeFileSync(PROXY_PID_FILE, String(process.pid));
|
|
82
|
+
process.stdout.write(`${JSON.stringify({
|
|
83
|
+
endpoint: proxyEndpoint
|
|
84
|
+
})}\n`);
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
//# sourceMappingURL=cdp-proxy.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cdp-proxy.mjs","sources":["../../src/cdp-proxy.ts"],"sourcesContent":["/**\n * CDP WebSocket Proxy — standalone process.\n *\n * Holds a single persistent WebSocket connection to Chrome's CDP endpoint and\n * exposes a local WebSocket server. Midscene CLI processes connect to the proxy\n * instead of Chrome directly, so Chrome's \"Allow remote debugging\" permission\n * popup only fires once (when the proxy connects).\n *\n * Exit conditions:\n * 1. Upstream Chrome connection closes or errors.\n * 2. No downstream client message for IDLE_TIMEOUT_MS (default 5 min).\n * 3. SIGTERM / SIGINT.\n *\n * Usage (spawned by mcp-tools-cdp.ts):\n * node cdp-proxy.js <chrome-ws-endpoint>\n *\n * On startup, prints the proxy endpoint to stdout as a single JSON line:\n * {\"endpoint\":\"ws://127.0.0.1:<port>/devtools/browser\"}\n * and writes the same endpoint to PROXY_ENDPOINT_FILE.\n */\n\nimport { existsSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs';\nimport { createServer } from 'node:http';\nimport WebSocket, { WebSocketServer } from 'ws';\nimport { PROXY_ENDPOINT_FILE, PROXY_PID_FILE } from './cdp-proxy-constants';\n\n// ---------------------------------------------------------------------------\n// Config\n// ---------------------------------------------------------------------------\n\nconst IDLE_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes\n\nconst chromeEndpoint = process.argv[2];\nif (!chromeEndpoint) {\n process.stderr.write('Usage: node cdp-proxy.js <chrome-ws-endpoint>\\n');\n process.exit(1);\n}\n\n// ---------------------------------------------------------------------------\n// Cleanup\n// ---------------------------------------------------------------------------\n\nfunction cleanupIfOwned() {\n try {\n if (existsSync(PROXY_PID_FILE)) {\n const pid = Number(readFileSync(PROXY_PID_FILE, 'utf-8').trim());\n if (pid !== process.pid) return;\n }\n } catch {}\n try {\n if (existsSync(PROXY_ENDPOINT_FILE)) unlinkSync(PROXY_ENDPOINT_FILE);\n } catch {}\n try {\n if (existsSync(PROXY_PID_FILE)) unlinkSync(PROXY_PID_FILE);\n } catch {}\n}\n\nfunction shutdown(reason: string) {\n process.stderr.write(`[cdp-proxy] shutting down: ${reason}\\n`);\n cleanupIfOwned();\n process.exit(0);\n}\n\nprocess.on('SIGTERM', () => shutdown('SIGTERM'));\nprocess.on('SIGINT', () => shutdown('SIGINT'));\nprocess.on('uncaughtException', (e) => shutdown(`uncaught: ${e.message}`));\n\n// ---------------------------------------------------------------------------\n// Idle timer\n// ---------------------------------------------------------------------------\n\nlet idleTimer: ReturnType<typeof setTimeout> | null = null;\n\nfunction resetIdleTimer() {\n if (idleTimer) clearTimeout(idleTimer);\n idleTimer = setTimeout(\n () => shutdown('idle timeout (5min)'),\n IDLE_TIMEOUT_MS,\n );\n}\n\nresetIdleTimer();\n\n// ---------------------------------------------------------------------------\n// Upstream: connect to Chrome\n// ---------------------------------------------------------------------------\n\nconst upstream = new WebSocket(chromeEndpoint);\nconst clients = new Set<WebSocket>();\n\nupstream.on('error', (err) => shutdown(`upstream error: ${err.message}`));\nupstream.on('close', () => shutdown('upstream closed'));\n\n// Forward upstream messages to all downstream clients\nupstream.on('message', (data, isBinary) => {\n resetIdleTimer();\n for (const client of clients) {\n if (client.readyState === WebSocket.OPEN) {\n client.send(data, { binary: isBinary });\n }\n }\n});\n\n// ---------------------------------------------------------------------------\n// Downstream: local WebSocket server\n// ---------------------------------------------------------------------------\n\nconst httpServer = createServer((_req, res) => {\n res.writeHead(404);\n res.end();\n});\n\nconst wss = new WebSocketServer({ server: httpServer });\n\nwss.on('connection', (clientWs) => {\n clients.add(clientWs);\n resetIdleTimer();\n\n // Forward downstream messages to upstream\n clientWs.on('message', (data, isBinary) => {\n resetIdleTimer();\n if (upstream.readyState === WebSocket.OPEN) {\n upstream.send(data, { binary: isBinary });\n }\n });\n\n clientWs.on('close', () => clients.delete(clientWs));\n clientWs.on('error', () => clients.delete(clientWs));\n});\n\n// ---------------------------------------------------------------------------\n// Start\n// ---------------------------------------------------------------------------\n\nupstream.on('open', () => {\n // Check for duplicate proxy\n if (existsSync(PROXY_PID_FILE)) {\n try {\n const existingPid = Number(readFileSync(PROXY_PID_FILE, 'utf-8').trim());\n if (existingPid !== process.pid) {\n try {\n process.kill(existingPid, 0);\n process.exit(0); // another proxy is alive\n } catch {\n // dead — we take over\n }\n }\n } catch {}\n }\n\n httpServer.listen(0, '127.0.0.1', () => {\n const addr = httpServer.address();\n if (!addr || typeof addr === 'string') {\n shutdown('failed to get server address');\n return;\n }\n\n const proxyEndpoint = `ws://127.0.0.1:${addr.port}/devtools/browser`;\n\n writeFileSync(PROXY_ENDPOINT_FILE, proxyEndpoint);\n writeFileSync(PROXY_PID_FILE, String(process.pid));\n\n process.stdout.write(`${JSON.stringify({ endpoint: proxyEndpoint })}\\n`);\n });\n});\n"],"names":["IDLE_TIMEOUT_MS","chromeEndpoint","process","cleanupIfOwned","existsSync","PROXY_PID_FILE","pid","Number","readFileSync","PROXY_ENDPOINT_FILE","unlinkSync","shutdown","reason","e","idleTimer","resetIdleTimer","clearTimeout","setTimeout","upstream","WebSocket","clients","Set","err","data","isBinary","client","httpServer","createServer","_req","res","wss","WebSocketServer","clientWs","existingPid","addr","proxyEndpoint","writeFileSync","String","JSON"],"mappings":"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"}
|
package/dist/es/cli.mjs
CHANGED
|
@@ -39,7 +39,7 @@ tools = isBridge ? new WebMidsceneTools() : isCdp ? new WebCdpMidsceneTools(cdpE
|
|
|
39
39
|
runToolsCLI(tools, 'midscene-web', {
|
|
40
40
|
stripPrefix: 'web_',
|
|
41
41
|
argv,
|
|
42
|
-
version: "1.
|
|
42
|
+
version: "1.6.0"
|
|
43
43
|
}).catch((e)=>{
|
|
44
44
|
if (!(e instanceof CLIError)) console.error(e);
|
|
45
45
|
process.exit(e instanceof CLIError ? e.exitCode : 1);
|
package/dist/es/mcp-server.mjs
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
1
4
|
import { ScreenshotItem, z } from "@midscene/core";
|
|
2
5
|
import { BaseMidsceneTools } from "@midscene/shared/mcp";
|
|
3
6
|
import puppeteer_core from "puppeteer-core";
|
|
7
|
+
import { PROXY_ENDPOINT_FILE, PROXY_PID_FILE } from "./cdp-proxy-constants.mjs";
|
|
4
8
|
import { PuppeteerAgent } from "./puppeteer/index.mjs";
|
|
5
9
|
import { StaticPage } from "./static/index.mjs";
|
|
6
10
|
function _define_property(obj, key, value) {
|
|
@@ -13,6 +17,90 @@ function _define_property(obj, key, value) {
|
|
|
13
17
|
else obj[key] = value;
|
|
14
18
|
return obj;
|
|
15
19
|
}
|
|
20
|
+
function isProxyAlive() {
|
|
21
|
+
if (!existsSync(PROXY_PID_FILE)) return false;
|
|
22
|
+
try {
|
|
23
|
+
const pid = Number(readFileSync(PROXY_PID_FILE, 'utf-8').trim());
|
|
24
|
+
process.kill(pid, 0);
|
|
25
|
+
return true;
|
|
26
|
+
} catch {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
function readProxyEndpoint() {
|
|
31
|
+
if (!existsSync(PROXY_ENDPOINT_FILE)) return null;
|
|
32
|
+
try {
|
|
33
|
+
return readFileSync(PROXY_ENDPOINT_FILE, 'utf-8').trim();
|
|
34
|
+
} catch {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
function spawnProxy(chromeEndpoint) {
|
|
39
|
+
return new Promise((resolve, reject)=>{
|
|
40
|
+
const proxyScript = join(__dirname, 'cdp-proxy.js');
|
|
41
|
+
const proc = spawn(process.execPath, [
|
|
42
|
+
proxyScript,
|
|
43
|
+
chromeEndpoint
|
|
44
|
+
], {
|
|
45
|
+
detached: true,
|
|
46
|
+
stdio: [
|
|
47
|
+
'ignore',
|
|
48
|
+
'pipe',
|
|
49
|
+
'ignore'
|
|
50
|
+
]
|
|
51
|
+
});
|
|
52
|
+
proc.unref();
|
|
53
|
+
let output = '';
|
|
54
|
+
let settled = false;
|
|
55
|
+
const timer = setTimeout(()=>{
|
|
56
|
+
if (!settled) {
|
|
57
|
+
settled = true;
|
|
58
|
+
reject(new Error('Proxy startup timeout (10s)'));
|
|
59
|
+
}
|
|
60
|
+
}, 10000);
|
|
61
|
+
const onData = (chunk)=>{
|
|
62
|
+
output += chunk.toString();
|
|
63
|
+
const lines = output.split('\n');
|
|
64
|
+
for (const line of lines)if (line.trim()) try {
|
|
65
|
+
const parsed = JSON.parse(line);
|
|
66
|
+
if (parsed.endpoint && !settled) {
|
|
67
|
+
settled = true;
|
|
68
|
+
clearTimeout(timer);
|
|
69
|
+
proc.stdout.removeListener('data', onData);
|
|
70
|
+
resolve(parsed.endpoint);
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
} catch {}
|
|
74
|
+
};
|
|
75
|
+
proc.stdout.on('data', onData);
|
|
76
|
+
proc.on('error', (err)=>{
|
|
77
|
+
if (!settled) {
|
|
78
|
+
settled = true;
|
|
79
|
+
clearTimeout(timer);
|
|
80
|
+
reject(new Error(`Failed to spawn proxy: ${err.message}`));
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
proc.on('exit', (code)=>{
|
|
84
|
+
if (!settled) {
|
|
85
|
+
settled = true;
|
|
86
|
+
clearTimeout(timer);
|
|
87
|
+
reject(new Error(`Proxy exited with code ${code} before ready`));
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
async function getProxyEndpoint(chromeEndpoint) {
|
|
93
|
+
if (isProxyAlive()) {
|
|
94
|
+
const endpoint = readProxyEndpoint();
|
|
95
|
+
if (endpoint) return endpoint;
|
|
96
|
+
}
|
|
97
|
+
try {
|
|
98
|
+
return await spawnProxy(chromeEndpoint);
|
|
99
|
+
} catch (err) {
|
|
100
|
+
console.warn(`[cdp] proxy failed, falling back to direct connection: ${err}`);
|
|
101
|
+
return chromeEndpoint;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
16
104
|
class WebCdpMidsceneTools extends BaseMidsceneTools {
|
|
17
105
|
createTemporaryDevice() {
|
|
18
106
|
return new StaticPage({
|
|
@@ -34,21 +122,32 @@ class WebCdpMidsceneTools extends BaseMidsceneTools {
|
|
|
34
122
|
this.agent = void 0;
|
|
35
123
|
}
|
|
36
124
|
if (this.agent) return this.agent;
|
|
37
|
-
if (!this.activeBrowser)
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
125
|
+
if (!this.activeBrowser) {
|
|
126
|
+
const endpoint = await getProxyEndpoint(this.cdpEndpoint);
|
|
127
|
+
this.activeBrowser = await puppeteer_core.connect({
|
|
128
|
+
browserWSEndpoint: endpoint,
|
|
129
|
+
defaultViewport: null
|
|
130
|
+
});
|
|
131
|
+
}
|
|
41
132
|
const browser = this.activeBrowser;
|
|
42
133
|
const pages = await browser.pages();
|
|
134
|
+
const webPages = pages.filter((p)=>/^https?:\/\//.test(p.url()));
|
|
43
135
|
let page;
|
|
44
|
-
if (navigateToUrl) {
|
|
45
|
-
page =
|
|
136
|
+
if (navigateToUrl) if (webPages.length > 0) {
|
|
137
|
+
page = webPages[webPages.length - 1];
|
|
138
|
+
await page.bringToFront();
|
|
46
139
|
await page.goto(navigateToUrl, {
|
|
47
140
|
timeout: 30000,
|
|
48
141
|
waitUntil: 'domcontentloaded'
|
|
49
142
|
});
|
|
50
143
|
} else {
|
|
51
|
-
|
|
144
|
+
page = await browser.newPage();
|
|
145
|
+
await page.goto(navigateToUrl, {
|
|
146
|
+
timeout: 30000,
|
|
147
|
+
waitUntil: 'domcontentloaded'
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
else {
|
|
52
151
|
page = webPages.length > 0 ? webPages[webPages.length - 1] : pages[pages.length - 1] || await browser.newPage();
|
|
53
152
|
await page.bringToFront();
|
|
54
153
|
}
|
|
@@ -75,7 +174,9 @@ class WebCdpMidsceneTools extends BaseMidsceneTools {
|
|
|
75
174
|
if (this.agent) {
|
|
76
175
|
try {
|
|
77
176
|
await this.agent.destroy?.();
|
|
78
|
-
} catch
|
|
177
|
+
} catch (e) {
|
|
178
|
+
console.debug('Failed to destroy agent during connect:', e);
|
|
179
|
+
}
|
|
79
180
|
this.agent = void 0;
|
|
80
181
|
}
|
|
81
182
|
this.agent = await this.ensureAgent(url);
|
|
@@ -100,7 +201,9 @@ class WebCdpMidsceneTools extends BaseMidsceneTools {
|
|
|
100
201
|
if (this.agent) {
|
|
101
202
|
try {
|
|
102
203
|
await this.agent.destroy?.();
|
|
103
|
-
} catch
|
|
204
|
+
} catch (e) {
|
|
205
|
+
console.debug('Failed to destroy agent during disconnect:', e);
|
|
206
|
+
}
|
|
104
207
|
this.agent = void 0;
|
|
105
208
|
}
|
|
106
209
|
if (this.activeBrowser) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mcp-tools-cdp.mjs","sources":["../../src/mcp-tools-cdp.ts"],"sourcesContent":["import { ScreenshotItem, z } from '@midscene/core';\nimport { BaseMidsceneTools, type ToolDefinition } from '@midscene/shared/mcp';\nimport type { Page as PuppeteerPage } from 'puppeteer';\nimport puppeteer from 'puppeteer-core';\nimport type { Browser, Page } from 'puppeteer-core';\nimport { PuppeteerAgent } from './puppeteer';\nimport { StaticPage } from './static';\n\n/**\n * Tools manager for Web CDP-mode MCP.\n * Connects to an existing Chrome browser via CDP (Chrome DevTools Protocol) endpoint.\n * Unlike WebPuppeteerMidsceneTools which launches its own Chrome, this connects\n * to a browser that is already running with remote debugging enabled.\n */\nexport class WebCdpMidsceneTools extends BaseMidsceneTools<PuppeteerAgent> {\n private cdpEndpoint: string;\n private activeBrowser: Browser | null = null;\n\n constructor(cdpEndpoint: string) {\n super();\n this.cdpEndpoint = cdpEndpoint;\n }\n\n protected createTemporaryDevice() {\n return new StaticPage({\n screenshot: ScreenshotItem.create('', Date.now()),\n shotSize: { width: 1920, height: 1080 },\n shrunkShotToLogicalRatio: 1,\n });\n }\n\n protected async ensureAgent(navigateToUrl?: string): Promise<PuppeteerAgent> {\n // Re-init if URL provided\n if (this.agent && navigateToUrl) {\n try {\n await this.agent?.destroy?.();\n } catch (error) {\n console.debug('Failed to destroy agent during re-init:', error);\n }\n this.agent = undefined;\n }\n\n if (this.agent) return this.agent;\n\n // Connect to the existing browser via CDP endpoint\n if (!this.activeBrowser) {\n this.activeBrowser = await puppeteer.connect({\n browserWSEndpoint: this.cdpEndpoint,\n defaultViewport: null,\n });\n }\n\n const browser = this.activeBrowser;\n const pages = await browser.pages();\n let page: Page;\n\n if (navigateToUrl) {\n page = await browser.newPage();\n await page.goto(navigateToUrl, {\n timeout: 30000,\n waitUntil: 'domcontentloaded',\n });\n } else {\n // Reuse the last web page\n const webPages = pages.filter((p) => /^https?:\\/\\//.test(p.url()));\n page =\n webPages.length > 0\n ? webPages[webPages.length - 1]\n : pages[pages.length - 1] || (await browser.newPage());\n\n await page.bringToFront();\n }\n\n this.agent = new PuppeteerAgent(page as unknown as PuppeteerPage);\n return this.agent;\n }\n\n public async destroy(): Promise<void> {\n await super.destroy();\n if (this.activeBrowser) {\n this.activeBrowser.disconnect();\n this.activeBrowser = null;\n }\n }\n\n protected preparePlatformTools(): ToolDefinition[] {\n return [\n {\n name: 'web_connect',\n description:\n 'Connect to a web page via CDP. Opens a new tab with the given URL, or reuses the current page.',\n schema: {\n url: z\n .string()\n .url()\n .optional()\n .describe('URL to open in new tab (omit to use current page)'),\n },\n handler: async (args) => {\n const { url } = args as { url?: string };\n\n // Destroy existing agent\n if (this.agent) {\n try {\n await this.agent.destroy?.();\n } catch {}\n this.agent = undefined;\n }\n\n this.agent = await this.ensureAgent(url);\n\n const screenshot = await this.agent.page?.screenshotBase64();\n const label = url ?? 'current page';\n\n return {\n content: [\n { type: 'text', text: `Connected via CDP to: ${label}` },\n ...(screenshot ? this.buildScreenshotContent(screenshot) : []),\n ],\n };\n },\n },\n {\n name: 'web_disconnect',\n description:\n 'Disconnect from current web page. The browser stays running (managed externally).',\n schema: {},\n handler: async () => {\n if (this.agent) {\n try {\n await this.agent.destroy?.();\n } catch {}\n this.agent = undefined;\n }\n if (this.activeBrowser) {\n this.activeBrowser.disconnect();\n this.activeBrowser = null;\n }\n return this.buildTextResult(\n 'Disconnected from web page (browser still running externally)',\n );\n },\n },\n ];\n }\n}\n"],"names":["WebCdpMidsceneTools","BaseMidsceneTools","StaticPage","ScreenshotItem","Date","navigateToUrl","error","console","undefined","puppeteer","browser","pages","page","webPages","p","PuppeteerAgent","z","args","url","screenshot","label","cdpEndpoint"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"mcp-tools-cdp.mjs","sources":["../../src/mcp-tools-cdp.ts"],"sourcesContent":["import { spawn } from 'node:child_process';\nimport { existsSync, readFileSync } from 'node:fs';\nimport { join } from 'node:path';\nimport { ScreenshotItem, z } from '@midscene/core';\nimport { BaseMidsceneTools, type ToolDefinition } from '@midscene/shared/mcp';\nimport type { Page as PuppeteerPage } from 'puppeteer';\nimport puppeteer from 'puppeteer-core';\nimport type { Browser, Page } from 'puppeteer-core';\nimport { PROXY_ENDPOINT_FILE, PROXY_PID_FILE } from './cdp-proxy-constants';\nimport { PuppeteerAgent } from './puppeteer';\nimport { StaticPage } from './static';\n\n/**\n * Check if a previously spawned proxy process is still alive.\n */\nfunction isProxyAlive(): boolean {\n if (!existsSync(PROXY_PID_FILE)) return false;\n try {\n const pid = Number(readFileSync(PROXY_PID_FILE, 'utf-8').trim());\n process.kill(pid, 0); // signal 0 = existence check\n return true;\n } catch {\n return false;\n }\n}\n\n/**\n * Read the proxy endpoint written by cdp-proxy.ts.\n */\nfunction readProxyEndpoint(): string | null {\n if (!existsSync(PROXY_ENDPOINT_FILE)) return null;\n try {\n return readFileSync(PROXY_ENDPOINT_FILE, 'utf-8').trim();\n } catch {\n return null;\n }\n}\n\n/**\n * Spawn the CDP proxy process and wait for it to print the endpoint.\n */\nfunction spawnProxy(chromeEndpoint: string): Promise<string> {\n return new Promise((resolve, reject) => {\n const proxyScript = join(__dirname, 'cdp-proxy.js');\n const proc = spawn(process.execPath, [proxyScript, chromeEndpoint], {\n detached: true,\n stdio: ['ignore', 'pipe', 'ignore'],\n });\n proc.unref();\n\n let output = '';\n let settled = false;\n const timer = setTimeout(() => {\n if (!settled) {\n settled = true;\n reject(new Error('Proxy startup timeout (10s)'));\n }\n }, 10000);\n\n const onData = (chunk: Buffer) => {\n output += chunk.toString();\n const lines = output.split('\\n');\n for (const line of lines) {\n if (!line.trim()) continue;\n try {\n const parsed = JSON.parse(line);\n if (parsed.endpoint && !settled) {\n settled = true;\n clearTimeout(timer);\n proc.stdout!.removeListener('data', onData);\n resolve(parsed.endpoint);\n return;\n }\n } catch {\n // stdout may contain non-JSON lines during startup — skip them\n }\n }\n };\n proc.stdout!.on('data', onData);\n\n proc.on('error', (err) => {\n if (!settled) {\n settled = true;\n clearTimeout(timer);\n reject(new Error(`Failed to spawn proxy: ${err.message}`));\n }\n });\n proc.on('exit', (code) => {\n if (!settled) {\n settled = true;\n clearTimeout(timer);\n reject(new Error(`Proxy exited with code ${code} before ready`));\n }\n });\n });\n}\n\n/**\n * Get the proxy endpoint, spawning the proxy if needed.\n * Falls back to direct connection if proxy cannot be started.\n */\nasync function getProxyEndpoint(chromeEndpoint: string): Promise<string> {\n // If proxy is alive and endpoint file exists, reuse it\n if (isProxyAlive()) {\n const endpoint = readProxyEndpoint();\n if (endpoint) return endpoint;\n }\n\n // Spawn a new proxy\n try {\n return await spawnProxy(chromeEndpoint);\n } catch (err) {\n console.warn(\n `[cdp] proxy failed, falling back to direct connection: ${err}`,\n );\n return chromeEndpoint;\n }\n}\n\n/**\n * Tools manager for Web CDP-mode MCP.\n * Connects to an existing Chrome browser via CDP (Chrome DevTools Protocol) endpoint.\n * Unlike WebPuppeteerMidsceneTools which launches its own Chrome, this connects\n * to a browser that is already running with remote debugging enabled.\n *\n * Uses a persistent WebSocket proxy to avoid repeated Chrome permission popups\n * when Chrome's settings-based remote debugging is used.\n */\nexport class WebCdpMidsceneTools extends BaseMidsceneTools<PuppeteerAgent> {\n private cdpEndpoint: string;\n private activeBrowser: Browser | null = null;\n\n constructor(cdpEndpoint: string) {\n super();\n this.cdpEndpoint = cdpEndpoint;\n }\n\n protected createTemporaryDevice() {\n return new StaticPage({\n screenshot: ScreenshotItem.create('', Date.now()),\n shotSize: { width: 1920, height: 1080 },\n shrunkShotToLogicalRatio: 1,\n });\n }\n\n protected async ensureAgent(navigateToUrl?: string): Promise<PuppeteerAgent> {\n // Re-init if URL provided\n if (this.agent && navigateToUrl) {\n try {\n await this.agent?.destroy?.();\n } catch (error) {\n console.debug('Failed to destroy agent during re-init:', error);\n }\n this.agent = undefined;\n }\n\n if (this.agent) return this.agent;\n\n // Connect via proxy to avoid repeated Chrome permission popups\n if (!this.activeBrowser) {\n const endpoint = await getProxyEndpoint(this.cdpEndpoint);\n this.activeBrowser = await puppeteer.connect({\n browserWSEndpoint: endpoint,\n defaultViewport: null,\n });\n }\n\n const browser = this.activeBrowser;\n const pages = await browser.pages();\n const webPages = pages.filter((p) => /^https?:\\/\\//.test(p.url()));\n let page: Page;\n\n if (navigateToUrl) {\n if (webPages.length > 0) {\n // Reuse an existing page and navigate it — avoids creating invisible\n // tabs when Chrome uses settings-based remote debugging (no HTTP\n // discovery endpoints, /devtools/page/* returns 403).\n page = webPages[webPages.length - 1];\n await page.bringToFront();\n await page.goto(navigateToUrl, {\n timeout: 30000,\n waitUntil: 'domcontentloaded',\n });\n } else {\n // No existing web pages — fall back to creating a new tab\n page = await browser.newPage();\n await page.goto(navigateToUrl, {\n timeout: 30000,\n waitUntil: 'domcontentloaded',\n });\n }\n } else {\n // Reuse the last web page\n page =\n webPages.length > 0\n ? webPages[webPages.length - 1]\n : pages[pages.length - 1] || (await browser.newPage());\n\n await page.bringToFront();\n }\n\n this.agent = new PuppeteerAgent(page as unknown as PuppeteerPage);\n return this.agent;\n }\n\n public async destroy(): Promise<void> {\n await super.destroy();\n if (this.activeBrowser) {\n this.activeBrowser.disconnect();\n this.activeBrowser = null;\n }\n }\n\n protected preparePlatformTools(): ToolDefinition[] {\n return [\n {\n name: 'web_connect',\n description:\n 'Connect to a web page via CDP. Opens a new tab with the given URL, or reuses the current page.',\n schema: {\n url: z\n .string()\n .url()\n .optional()\n .describe('URL to open in new tab (omit to use current page)'),\n },\n handler: async (args) => {\n const { url } = args as { url?: string };\n\n // Destroy existing agent\n if (this.agent) {\n try {\n await this.agent.destroy?.();\n } catch (e) {\n console.debug('Failed to destroy agent during connect:', e);\n }\n this.agent = undefined;\n }\n\n this.agent = await this.ensureAgent(url);\n\n const screenshot = await this.agent.page?.screenshotBase64();\n const label = url ?? 'current page';\n\n return {\n content: [\n { type: 'text', text: `Connected via CDP to: ${label}` },\n ...(screenshot ? this.buildScreenshotContent(screenshot) : []),\n ],\n };\n },\n },\n {\n name: 'web_disconnect',\n description:\n 'Disconnect from current web page. The browser stays running (managed externally).',\n schema: {},\n handler: async () => {\n if (this.agent) {\n try {\n await this.agent.destroy?.();\n } catch (e) {\n console.debug('Failed to destroy agent during disconnect:', e);\n }\n this.agent = undefined;\n }\n if (this.activeBrowser) {\n this.activeBrowser.disconnect();\n this.activeBrowser = null;\n }\n return this.buildTextResult(\n 'Disconnected from web page (browser still running externally)',\n );\n },\n },\n ];\n }\n}\n"],"names":["isProxyAlive","existsSync","PROXY_PID_FILE","pid","Number","readFileSync","process","readProxyEndpoint","PROXY_ENDPOINT_FILE","spawnProxy","chromeEndpoint","Promise","resolve","reject","proxyScript","join","__dirname","proc","spawn","output","settled","timer","setTimeout","Error","onData","chunk","lines","line","parsed","JSON","clearTimeout","err","code","getProxyEndpoint","endpoint","console","WebCdpMidsceneTools","BaseMidsceneTools","StaticPage","ScreenshotItem","Date","navigateToUrl","error","undefined","puppeteer","browser","pages","webPages","p","page","PuppeteerAgent","z","args","url","e","screenshot","label","cdpEndpoint"],"mappings":"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"}
|
|
@@ -98,7 +98,7 @@ class MidsceneReporter {
|
|
|
98
98
|
const tpl = getReportTpl();
|
|
99
99
|
if (!tpl) throw new Error('Report template not found. Ensure @midscene/core is built correctly.');
|
|
100
100
|
let dumpScript = `<script type="midscene_web_dump">\n${escapeScriptTag(testData.dumpString)}\n</script>`;
|
|
101
|
-
if (
|
|
101
|
+
if (testData.attributes) {
|
|
102
102
|
const attributesArr = Object.keys(testData.attributes).map((key)=>`${key}="${encodeURIComponent(testData.attributes[key])}"`);
|
|
103
103
|
dumpScript = dumpScript.replace('<script type="midscene_web_dump"', `<script type="midscene_web_dump" ${attributesArr.join(' ')}`);
|
|
104
104
|
}
|
|
@@ -150,6 +150,7 @@ class MidsceneReporter {
|
|
|
150
150
|
const testData = {
|
|
151
151
|
dumpString,
|
|
152
152
|
attributes: {
|
|
153
|
+
'data-group-id': testId,
|
|
153
154
|
playwright_test_id: testId,
|
|
154
155
|
playwright_test_title: `${test.title}${projectSuffix}${retry}`,
|
|
155
156
|
playwright_test_status: result.status,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"playwright/reporter/index.mjs","sources":["../../../../src/playwright/reporter/index.ts"],"sourcesContent":["import {\n copyFileSync,\n existsSync,\n mkdirSync,\n rmSync,\n writeFileSync,\n} from 'node:fs';\nimport { dirname, join } from 'node:path';\nimport {\n GroupedActionDump,\n type ReportDumpWithAttributes,\n} from '@midscene/core';\nimport { getReportFileName, printReportMsg } from '@midscene/core/agent';\nimport { getReportTpl } from '@midscene/core/utils';\nimport { getMidsceneRunSubDir } from '@midscene/shared/common';\nimport {\n escapeScriptTag,\n replaceIllegalPathCharsAndSpace,\n} from '@midscene/shared/utils';\nimport type {\n FullConfig,\n Reporter,\n Suite,\n TestCase,\n TestResult,\n} from '@playwright/test/reporter';\n\ninterface MidsceneReporterOptions {\n type?: 'merged' | 'separate';\n /**\n * Output format for the report.\n * - 'single-html': All screenshots embedded as base64 in a single HTML file (default)\n * - 'html-and-external-assets': Screenshots saved as separate PNG files in a screenshots/ subdirectory\n *\n * Note: 'html-and-external-assets' reports must be served via HTTP server due to CORS restrictions.\n */\n outputFormat?: 'single-html' | 'html-and-external-assets';\n}\n\nclass MidsceneReporter implements Reporter {\n private mergedFilename?: string;\n private testTitleToFilename = new Map<string, string>();\n mode?: 'merged' | 'separate';\n outputFormat: 'single-html' | 'html-and-external-assets';\n\n // Track all temp files created during this test run for cleanup\n private tempFiles = new Set<string>();\n\n // Track pending report updates\n private pendingReports = new Set<Promise<void>>();\n\n // Track whether the merged report file has been initialized\n private mergedReportInitialized = false;\n\n // Write queue to serialize file writes and prevent concurrent write conflicts\n private writeQueue: Promise<void> = Promise.resolve();\n\n // Track whether we have multiple projects (browsers)\n private hasMultipleProjects = false;\n\n // Track written screenshots to avoid duplicates (for directory mode)\n private writtenScreenshots = new Set<string>();\n\n constructor(options: MidsceneReporterOptions = {}) {\n // Set mode from constructor options (official Playwright way)\n this.mode = MidsceneReporter.getMode(options.type ?? 'merged');\n this.outputFormat = options.outputFormat ?? 'single-html';\n }\n\n private static getMode(reporterType: string): 'merged' | 'separate' {\n if (!reporterType) {\n return 'merged';\n }\n if (reporterType !== 'merged' && reporterType !== 'separate') {\n throw new Error(\n `Unknown reporter type in playwright config: ${reporterType}, only support 'merged' or 'separate'`,\n );\n }\n return reporterType;\n }\n\n private getSeparatedFilename(testTitle: string): string {\n if (!this.testTitleToFilename.has(testTitle)) {\n const baseTag = `playwright-${replaceIllegalPathCharsAndSpace(testTitle)}`;\n const generatedFilename = getReportFileName(baseTag);\n this.testTitleToFilename.set(testTitle, generatedFilename);\n }\n return this.testTitleToFilename.get(testTitle)!;\n }\n\n private getReportFilename(testTitle?: string): string {\n if (this.mode === 'merged') {\n if (!this.mergedFilename) {\n this.mergedFilename = getReportFileName('playwright-merged');\n }\n return this.mergedFilename;\n } else if (this.mode === 'separate') {\n if (!testTitle) throw new Error('testTitle is required in separate mode');\n return this.getSeparatedFilename(testTitle);\n }\n throw new Error(`Unknown mode: ${this.mode}`);\n }\n\n /**\n * Get the report path - for directory mode, returns a directory path with index.html\n */\n private getReportPath(testTitle?: string): string {\n const fileName = this.getReportFilename(testTitle);\n if (this.outputFormat === 'html-and-external-assets') {\n // Directory mode: report-name/index.html\n return join(getMidsceneRunSubDir('report'), fileName, 'index.html');\n }\n // Inline mode: report-name.html\n return join(getMidsceneRunSubDir('report'), `${fileName}.html`);\n }\n\n /**\n * Copy screenshots from temp location to report screenshots directory\n */\n private copyScreenshotsToReport(\n tempFilePath: string,\n reportPath: string,\n ): void {\n const screenshotsDir = join(dirname(reportPath), 'screenshots');\n const tempScreenshotsDir = `${tempFilePath}.screenshots`;\n\n if (!existsSync(tempScreenshotsDir)) {\n return;\n }\n\n // Ensure screenshots directory exists\n if (!existsSync(screenshotsDir)) {\n mkdirSync(screenshotsDir, { recursive: true });\n }\n\n // Read screenshot map to get all screenshot IDs\n const screenshotMapPath = `${tempFilePath}.screenshots.json`;\n if (!existsSync(screenshotMapPath)) {\n return;\n }\n\n try {\n const { readFileSync } = require('node:fs');\n const screenshotMap: Record<string, string> = JSON.parse(\n readFileSync(screenshotMapPath, 'utf-8'),\n );\n\n for (const [id, srcPath] of Object.entries(screenshotMap)) {\n // In merged mode, skip if already written to avoid duplicates\n // In separate mode, each test has its own screenshots directory\n if (this.mode === 'merged' && this.writtenScreenshots.has(id)) {\n continue;\n }\n\n const destPath = join(screenshotsDir, `${id}.png`);\n\n if (existsSync(srcPath)) {\n copyFileSync(srcPath, destPath);\n if (this.mode === 'merged') {\n this.writtenScreenshots.add(id);\n }\n }\n }\n } catch (error) {\n console.error('Error copying screenshots:', error);\n }\n }\n\n private async updateReport(testData: ReportDumpWithAttributes) {\n if (!testData || !this.mode) return;\n\n // Queue the write operation to prevent concurrent writes to the same file\n this.writeQueue = this.writeQueue.then(async () => {\n const reportPath = this.getReportPath(\n testData.attributes?.playwright_test_title,\n );\n\n // Ensure report directory exists for directory mode\n if (this.outputFormat === 'html-and-external-assets') {\n const reportDir = dirname(reportPath);\n if (!existsSync(reportDir)) {\n mkdirSync(reportDir, { recursive: true });\n }\n }\n\n // Get report template\n const tpl = getReportTpl();\n if (!tpl) {\n throw new Error(\n 'Report template not found. Ensure @midscene/core is built correctly.',\n );\n }\n\n // Parse the dump string and generate dump script tag\n let dumpScript = `<script type=\"midscene_web_dump\">\\n${escapeScriptTag(testData.dumpString)}\\n</script>`;\n\n // Add attributes to the dump script if this is merged report\n if (this.mode === 'merged' && testData.attributes) {\n const attributesArr = Object.keys(testData.attributes).map((key) => {\n return `${key}=\"${encodeURIComponent(testData.attributes![key])}\"`;\n });\n // Add attributes to the script tag\n dumpScript = dumpScript.replace(\n '<script type=\"midscene_web_dump\"',\n `<script type=\"midscene_web_dump\" ${attributesArr.join(' ')}`,\n );\n }\n\n // Write or append to file\n if (this.mode === 'merged') {\n // For merged report, write template + dump on first write, then only append dumps\n if (!this.mergedReportInitialized) {\n writeFileSync(reportPath, tpl + dumpScript, { flag: 'w' });\n this.mergedReportInitialized = true;\n } else {\n // Append only the dump scripts for subsequent tests\n writeFileSync(reportPath, dumpScript, { flag: 'a' });\n }\n } else {\n // For separate reports, write each test to its own file with template\n writeFileSync(reportPath, tpl + dumpScript, { flag: 'w' });\n }\n\n printReportMsg(reportPath);\n });\n\n await this.writeQueue;\n }\n\n async onBegin(config: FullConfig, suite: Suite) {\n // Check if we have multiple projects to determine if we need browser labels\n this.hasMultipleProjects = (config.projects?.length || 0) > 1;\n }\n\n onTestBegin(_test: TestCase, _result: TestResult) {\n // logger(`Starting test ${test.title}`);\n }\n\n onTestEnd(test: TestCase, result: TestResult) {\n const dumpAnnotation = test.annotations.find((annotation) => {\n return annotation.type === 'MIDSCENE_DUMP_ANNOTATION';\n });\n if (!dumpAnnotation?.description) return;\n\n const tempFilePath = dumpAnnotation.description;\n\n // Track temp files for potential cleanup in onEnd\n for (const filePath of GroupedActionDump.getFilePaths(tempFilePath)) {\n this.tempFiles.add(filePath);\n }\n\n let dumpString: string | undefined;\n\n try {\n if (this.outputFormat === 'html-and-external-assets') {\n // Directory mode: keep { $screenshot: id } format, copy screenshots to report dir\n const { readFileSync } = require('node:fs');\n dumpString = readFileSync(tempFilePath, 'utf-8');\n\n // Get report path and copy screenshots\n const retry = result.retry ? `(retry #${result.retry})` : '';\n const projectName = this.hasMultipleProjects\n ? test.parent?.project()?.name\n : undefined;\n const projectSuffix = projectName ? ` [${projectName}]` : '';\n const testTitle = `${test.title}${projectSuffix}${retry}`;\n const reportPath = this.getReportPath(testTitle);\n\n this.copyScreenshotsToReport(tempFilePath, reportPath);\n } else {\n // Inline mode: convert screenshots to base64\n dumpString = GroupedActionDump.fromFilesAsInlineJson(tempFilePath);\n }\n } catch (error) {\n console.error(\n `Failed to read Midscene dump file: ${tempFilePath}`,\n error,\n );\n // Don't return here - we still need to clean up the temp file\n }\n\n // Only update report if we successfully read the dump\n if (dumpString) {\n const retry = result.retry ? `(retry #${result.retry})` : '';\n const testId = `${test.id}${retry}`;\n\n // Get the project name (browser name) only if we have multiple projects\n const projectName = this.hasMultipleProjects\n ? test.parent?.project()?.name\n : undefined;\n const projectSuffix = projectName ? ` [${projectName}]` : '';\n\n const testData: ReportDumpWithAttributes = {\n dumpString,\n attributes: {\n playwright_test_id: testId,\n playwright_test_title: `${test.title}${projectSuffix}${retry}`,\n playwright_test_status: result.status,\n playwright_test_duration: result.duration,\n },\n };\n\n // Start async report update and track it\n const reportPromise = this.updateReport(testData)\n .catch((error) => {\n console.error('Error updating report:', error);\n })\n .finally(() => {\n this.pendingReports.delete(reportPromise);\n });\n this.pendingReports.add(reportPromise);\n }\n\n // Always try to clean up temp files\n try {\n GroupedActionDump.cleanupFiles(tempFilePath);\n for (const filePath of GroupedActionDump.getFilePaths(tempFilePath)) {\n this.tempFiles.delete(filePath);\n }\n } catch {\n // Keep in tempFiles for cleanup in onEnd\n }\n }\n\n async onEnd() {\n // Wait for all pending report updates to complete\n if (this.pendingReports.size > 0) {\n console.log(\n `Midscene: Waiting for ${this.pendingReports.size} pending report(s) to complete...`,\n );\n await Promise.all(Array.from(this.pendingReports));\n }\n\n // Print directory mode notice (only for merged mode)\n if (\n this.outputFormat === 'html-and-external-assets' &&\n this.mode === 'merged'\n ) {\n const reportPath = this.getReportPath();\n const reportDir = dirname(reportPath);\n console.log('[Midscene] Directory report generated.');\n console.log(\n '[Midscene] Note: This report must be served via HTTP server due to CORS restrictions.',\n );\n console.log(`[Midscene] Example: npx serve ${reportDir}`);\n } else if (\n this.outputFormat === 'html-and-external-assets' &&\n this.mode === 'separate'\n ) {\n const reportBaseDir = getMidsceneRunSubDir('report');\n console.log('[Midscene] Directory reports generated.');\n console.log(\n '[Midscene] Note: Reports must be served via HTTP server due to CORS restrictions.',\n );\n console.log(`[Midscene] Example: npx serve ${reportBaseDir}`);\n }\n\n // Clean up any remaining temp files that weren't deleted in onTestEnd\n if (this.tempFiles.size > 0) {\n console.log(\n `Midscene: Cleaning up ${this.tempFiles.size} remaining temp file(s)...`,\n );\n\n for (const filePath of this.tempFiles) {\n try {\n rmSync(filePath, { force: true });\n } catch (error) {\n // Silently ignore - file may have been deleted already\n }\n }\n\n this.tempFiles.clear();\n }\n }\n}\n\nexport default MidsceneReporter;\n"],"names":["MidsceneReporter","reporterType","Error","testTitle","baseTag","replaceIllegalPathCharsAndSpace","generatedFilename","getReportFileName","fileName","join","getMidsceneRunSubDir","tempFilePath","reportPath","screenshotsDir","dirname","tempScreenshotsDir","existsSync","mkdirSync","screenshotMapPath","readFileSync","require","screenshotMap","JSON","id","srcPath","Object","destPath","copyFileSync","error","console","testData","reportDir","tpl","getReportTpl","dumpScript","escapeScriptTag","attributesArr","key","encodeURIComponent","writeFileSync","printReportMsg","config","suite","_test","_result","test","result","dumpAnnotation","annotation","filePath","GroupedActionDump","dumpString","retry","projectName","undefined","projectSuffix","testId","reportPromise","Promise","Array","reportBaseDir","rmSync","options","Map","Set"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"playwright/reporter/index.mjs","sources":["../../../../src/playwright/reporter/index.ts"],"sourcesContent":["import {\n copyFileSync,\n existsSync,\n mkdirSync,\n rmSync,\n writeFileSync,\n} from 'node:fs';\nimport { dirname, join } from 'node:path';\nimport {\n GroupedActionDump,\n type ReportDumpWithAttributes,\n} from '@midscene/core';\nimport { getReportFileName, printReportMsg } from '@midscene/core/agent';\nimport { getReportTpl } from '@midscene/core/utils';\nimport { getMidsceneRunSubDir } from '@midscene/shared/common';\nimport {\n escapeScriptTag,\n replaceIllegalPathCharsAndSpace,\n} from '@midscene/shared/utils';\nimport type {\n FullConfig,\n Reporter,\n Suite,\n TestCase,\n TestResult,\n} from '@playwright/test/reporter';\n\ninterface MidsceneReporterOptions {\n type?: 'merged' | 'separate';\n /**\n * Output format for the report.\n * - 'single-html': All screenshots embedded as base64 in a single HTML file (default)\n * - 'html-and-external-assets': Screenshots saved as separate PNG files in a screenshots/ subdirectory\n *\n * Note: 'html-and-external-assets' reports must be served via HTTP server due to CORS restrictions.\n */\n outputFormat?: 'single-html' | 'html-and-external-assets';\n}\n\nclass MidsceneReporter implements Reporter {\n private mergedFilename?: string;\n private testTitleToFilename = new Map<string, string>();\n mode?: 'merged' | 'separate';\n outputFormat: 'single-html' | 'html-and-external-assets';\n\n // Track all temp files created during this test run for cleanup\n private tempFiles = new Set<string>();\n\n // Track pending report updates\n private pendingReports = new Set<Promise<void>>();\n\n // Track whether the merged report file has been initialized\n private mergedReportInitialized = false;\n\n // Write queue to serialize file writes and prevent concurrent write conflicts\n private writeQueue: Promise<void> = Promise.resolve();\n\n // Track whether we have multiple projects (browsers)\n private hasMultipleProjects = false;\n\n // Track written screenshots to avoid duplicates (for directory mode)\n private writtenScreenshots = new Set<string>();\n\n constructor(options: MidsceneReporterOptions = {}) {\n // Set mode from constructor options (official Playwright way)\n this.mode = MidsceneReporter.getMode(options.type ?? 'merged');\n this.outputFormat = options.outputFormat ?? 'single-html';\n }\n\n private static getMode(reporterType: string): 'merged' | 'separate' {\n if (!reporterType) {\n return 'merged';\n }\n if (reporterType !== 'merged' && reporterType !== 'separate') {\n throw new Error(\n `Unknown reporter type in playwright config: ${reporterType}, only support 'merged' or 'separate'`,\n );\n }\n return reporterType;\n }\n\n private getSeparatedFilename(testTitle: string): string {\n if (!this.testTitleToFilename.has(testTitle)) {\n const baseTag = `playwright-${replaceIllegalPathCharsAndSpace(testTitle)}`;\n const generatedFilename = getReportFileName(baseTag);\n this.testTitleToFilename.set(testTitle, generatedFilename);\n }\n return this.testTitleToFilename.get(testTitle)!;\n }\n\n private getReportFilename(testTitle?: string): string {\n if (this.mode === 'merged') {\n if (!this.mergedFilename) {\n this.mergedFilename = getReportFileName('playwright-merged');\n }\n return this.mergedFilename;\n } else if (this.mode === 'separate') {\n if (!testTitle) throw new Error('testTitle is required in separate mode');\n return this.getSeparatedFilename(testTitle);\n }\n throw new Error(`Unknown mode: ${this.mode}`);\n }\n\n /**\n * Get the report path - for directory mode, returns a directory path with index.html\n */\n private getReportPath(testTitle?: string): string {\n const fileName = this.getReportFilename(testTitle);\n if (this.outputFormat === 'html-and-external-assets') {\n // Directory mode: report-name/index.html\n return join(getMidsceneRunSubDir('report'), fileName, 'index.html');\n }\n // Inline mode: report-name.html\n return join(getMidsceneRunSubDir('report'), `${fileName}.html`);\n }\n\n /**\n * Copy screenshots from temp location to report screenshots directory\n */\n private copyScreenshotsToReport(\n tempFilePath: string,\n reportPath: string,\n ): void {\n const screenshotsDir = join(dirname(reportPath), 'screenshots');\n const tempScreenshotsDir = `${tempFilePath}.screenshots`;\n\n if (!existsSync(tempScreenshotsDir)) {\n return;\n }\n\n // Ensure screenshots directory exists\n if (!existsSync(screenshotsDir)) {\n mkdirSync(screenshotsDir, { recursive: true });\n }\n\n // Read screenshot map to get all screenshot IDs\n const screenshotMapPath = `${tempFilePath}.screenshots.json`;\n if (!existsSync(screenshotMapPath)) {\n return;\n }\n\n try {\n const { readFileSync } = require('node:fs');\n const screenshotMap: Record<string, string> = JSON.parse(\n readFileSync(screenshotMapPath, 'utf-8'),\n );\n\n for (const [id, srcPath] of Object.entries(screenshotMap)) {\n // In merged mode, skip if already written to avoid duplicates\n // In separate mode, each test has its own screenshots directory\n if (this.mode === 'merged' && this.writtenScreenshots.has(id)) {\n continue;\n }\n\n const destPath = join(screenshotsDir, `${id}.png`);\n\n if (existsSync(srcPath)) {\n copyFileSync(srcPath, destPath);\n if (this.mode === 'merged') {\n this.writtenScreenshots.add(id);\n }\n }\n }\n } catch (error) {\n console.error('Error copying screenshots:', error);\n }\n }\n\n private async updateReport(testData: ReportDumpWithAttributes) {\n if (!testData || !this.mode) return;\n\n // Queue the write operation to prevent concurrent writes to the same file\n this.writeQueue = this.writeQueue.then(async () => {\n const reportPath = this.getReportPath(\n testData.attributes?.playwright_test_title,\n );\n\n // Ensure report directory exists for directory mode\n if (this.outputFormat === 'html-and-external-assets') {\n const reportDir = dirname(reportPath);\n if (!existsSync(reportDir)) {\n mkdirSync(reportDir, { recursive: true });\n }\n }\n\n // Get report template\n const tpl = getReportTpl();\n if (!tpl) {\n throw new Error(\n 'Report template not found. Ensure @midscene/core is built correctly.',\n );\n }\n\n // Parse the dump string and generate dump script tag\n let dumpScript = `<script type=\"midscene_web_dump\">\\n${escapeScriptTag(testData.dumpString)}\\n</script>`;\n\n if (testData.attributes) {\n const attributesArr = Object.keys(testData.attributes).map((key) => {\n return `${key}=\"${encodeURIComponent(testData.attributes![key])}\"`;\n });\n // Add attributes to the script tag\n dumpScript = dumpScript.replace(\n '<script type=\"midscene_web_dump\"',\n `<script type=\"midscene_web_dump\" ${attributesArr.join(' ')}`,\n );\n }\n\n // Write or append to file\n if (this.mode === 'merged') {\n // For merged report, write template + dump on first write, then only append dumps\n if (!this.mergedReportInitialized) {\n writeFileSync(reportPath, tpl + dumpScript, { flag: 'w' });\n this.mergedReportInitialized = true;\n } else {\n // Append only the dump scripts for subsequent tests\n writeFileSync(reportPath, dumpScript, { flag: 'a' });\n }\n } else {\n // For separate reports, write each test to its own file with template\n writeFileSync(reportPath, tpl + dumpScript, { flag: 'w' });\n }\n\n printReportMsg(reportPath);\n });\n\n await this.writeQueue;\n }\n\n async onBegin(config: FullConfig, suite: Suite) {\n // Check if we have multiple projects to determine if we need browser labels\n this.hasMultipleProjects = (config.projects?.length || 0) > 1;\n }\n\n onTestBegin(_test: TestCase, _result: TestResult) {\n // logger(`Starting test ${test.title}`);\n }\n\n onTestEnd(test: TestCase, result: TestResult) {\n const dumpAnnotation = test.annotations.find((annotation) => {\n return annotation.type === 'MIDSCENE_DUMP_ANNOTATION';\n });\n if (!dumpAnnotation?.description) return;\n\n const tempFilePath = dumpAnnotation.description;\n\n // Track temp files for potential cleanup in onEnd\n for (const filePath of GroupedActionDump.getFilePaths(tempFilePath)) {\n this.tempFiles.add(filePath);\n }\n\n let dumpString: string | undefined;\n\n try {\n if (this.outputFormat === 'html-and-external-assets') {\n // Directory mode: keep { $screenshot: id } format, copy screenshots to report dir\n const { readFileSync } = require('node:fs');\n dumpString = readFileSync(tempFilePath, 'utf-8');\n\n // Get report path and copy screenshots\n const retry = result.retry ? `(retry #${result.retry})` : '';\n const projectName = this.hasMultipleProjects\n ? test.parent?.project()?.name\n : undefined;\n const projectSuffix = projectName ? ` [${projectName}]` : '';\n const testTitle = `${test.title}${projectSuffix}${retry}`;\n const reportPath = this.getReportPath(testTitle);\n\n this.copyScreenshotsToReport(tempFilePath, reportPath);\n } else {\n // Inline mode: convert screenshots to base64\n dumpString = GroupedActionDump.fromFilesAsInlineJson(tempFilePath);\n }\n } catch (error) {\n console.error(\n `Failed to read Midscene dump file: ${tempFilePath}`,\n error,\n );\n // Don't return here - we still need to clean up the temp file\n }\n\n // Only update report if we successfully read the dump\n if (dumpString) {\n const retry = result.retry ? `(retry #${result.retry})` : '';\n const testId = `${test.id}${retry}`;\n\n // Get the project name (browser name) only if we have multiple projects\n const projectName = this.hasMultipleProjects\n ? test.parent?.project()?.name\n : undefined;\n const projectSuffix = projectName ? ` [${projectName}]` : '';\n\n const testData: ReportDumpWithAttributes = {\n dumpString,\n attributes: {\n 'data-group-id': testId,\n playwright_test_id: testId,\n playwright_test_title: `${test.title}${projectSuffix}${retry}`,\n playwright_test_status: result.status,\n playwright_test_duration: result.duration,\n },\n };\n\n // Start async report update and track it\n const reportPromise = this.updateReport(testData)\n .catch((error) => {\n console.error('Error updating report:', error);\n })\n .finally(() => {\n this.pendingReports.delete(reportPromise);\n });\n this.pendingReports.add(reportPromise);\n }\n\n // Always try to clean up temp files\n try {\n GroupedActionDump.cleanupFiles(tempFilePath);\n for (const filePath of GroupedActionDump.getFilePaths(tempFilePath)) {\n this.tempFiles.delete(filePath);\n }\n } catch {\n // Keep in tempFiles for cleanup in onEnd\n }\n }\n\n async onEnd() {\n // Wait for all pending report updates to complete\n if (this.pendingReports.size > 0) {\n console.log(\n `Midscene: Waiting for ${this.pendingReports.size} pending report(s) to complete...`,\n );\n await Promise.all(Array.from(this.pendingReports));\n }\n\n // Print directory mode notice (only for merged mode)\n if (\n this.outputFormat === 'html-and-external-assets' &&\n this.mode === 'merged'\n ) {\n const reportPath = this.getReportPath();\n const reportDir = dirname(reportPath);\n console.log('[Midscene] Directory report generated.');\n console.log(\n '[Midscene] Note: This report must be served via HTTP server due to CORS restrictions.',\n );\n console.log(`[Midscene] Example: npx serve ${reportDir}`);\n } else if (\n this.outputFormat === 'html-and-external-assets' &&\n this.mode === 'separate'\n ) {\n const reportBaseDir = getMidsceneRunSubDir('report');\n console.log('[Midscene] Directory reports generated.');\n console.log(\n '[Midscene] Note: Reports must be served via HTTP server due to CORS restrictions.',\n );\n console.log(`[Midscene] Example: npx serve ${reportBaseDir}`);\n }\n\n // Clean up any remaining temp files that weren't deleted in onTestEnd\n if (this.tempFiles.size > 0) {\n console.log(\n `Midscene: Cleaning up ${this.tempFiles.size} remaining temp file(s)...`,\n );\n\n for (const filePath of this.tempFiles) {\n try {\n rmSync(filePath, { force: true });\n } catch (error) {\n // Silently ignore - file may have been deleted already\n }\n }\n\n this.tempFiles.clear();\n }\n }\n}\n\nexport default MidsceneReporter;\n"],"names":["MidsceneReporter","reporterType","Error","testTitle","baseTag","replaceIllegalPathCharsAndSpace","generatedFilename","getReportFileName","fileName","join","getMidsceneRunSubDir","tempFilePath","reportPath","screenshotsDir","dirname","tempScreenshotsDir","existsSync","mkdirSync","screenshotMapPath","readFileSync","require","screenshotMap","JSON","id","srcPath","Object","destPath","copyFileSync","error","console","testData","reportDir","tpl","getReportTpl","dumpScript","escapeScriptTag","attributesArr","key","encodeURIComponent","writeFileSync","printReportMsg","config","suite","_test","_result","test","result","dumpAnnotation","annotation","filePath","GroupedActionDump","dumpString","retry","projectName","undefined","projectSuffix","testId","reportPromise","Promise","Array","reportBaseDir","rmSync","options","Map","Set"],"mappings":"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"}
|
|
@@ -115,7 +115,7 @@ class BridgeServer {
|
|
|
115
115
|
(0, shared_utils_namespaceObject.logMsg)('one client connected');
|
|
116
116
|
this.socket = socket;
|
|
117
117
|
const clientVersion = socket.handshake.query.version;
|
|
118
|
-
(0, shared_utils_namespaceObject.logMsg)(`Bridge connected, cli-side version v1.
|
|
118
|
+
(0, shared_utils_namespaceObject.logMsg)(`Bridge connected, cli-side version v1.6.0, browser-side version v${clientVersion}`);
|
|
119
119
|
socket.on(external_common_js_namespaceObject.BridgeEvent.CallResponse, (params)=>{
|
|
120
120
|
const id = params.id;
|
|
121
121
|
const response = params.response;
|
|
@@ -139,7 +139,7 @@ class BridgeServer {
|
|
|
139
139
|
setTimeout(()=>{
|
|
140
140
|
this.onConnect?.();
|
|
141
141
|
const payload = {
|
|
142
|
-
version: "1.
|
|
142
|
+
version: "1.6.0"
|
|
143
143
|
};
|
|
144
144
|
socket.emit(external_common_js_namespaceObject.BridgeEvent.Connected, payload);
|
|
145
145
|
Promise.resolve().then(()=>{
|
|
@@ -103,7 +103,7 @@ class ExtensionBridgePageBrowserSide extends page_js_default() {
|
|
|
103
103
|
throw new Error('Connection denied by user');
|
|
104
104
|
}
|
|
105
105
|
}
|
|
106
|
-
this.onLogMessage(`Bridge connected, cli-side version v${this.bridgeClient.serverVersion}, browser-side version v1.
|
|
106
|
+
this.onLogMessage(`Bridge connected, cli-side version v${this.bridgeClient.serverVersion}, browser-side version v1.6.0`, 'log');
|
|
107
107
|
}
|
|
108
108
|
async connect() {
|
|
109
109
|
return await this.setupBridgeClient();
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __webpack_require__ = {};
|
|
3
|
+
(()=>{
|
|
4
|
+
__webpack_require__.d = (exports1, definition)=>{
|
|
5
|
+
for(var key in definition)if (__webpack_require__.o(definition, key) && !__webpack_require__.o(exports1, key)) Object.defineProperty(exports1, key, {
|
|
6
|
+
enumerable: true,
|
|
7
|
+
get: definition[key]
|
|
8
|
+
});
|
|
9
|
+
};
|
|
10
|
+
})();
|
|
11
|
+
(()=>{
|
|
12
|
+
__webpack_require__.o = (obj, prop)=>Object.prototype.hasOwnProperty.call(obj, prop);
|
|
13
|
+
})();
|
|
14
|
+
(()=>{
|
|
15
|
+
__webpack_require__.r = (exports1)=>{
|
|
16
|
+
if ('undefined' != typeof Symbol && Symbol.toStringTag) Object.defineProperty(exports1, Symbol.toStringTag, {
|
|
17
|
+
value: 'Module'
|
|
18
|
+
});
|
|
19
|
+
Object.defineProperty(exports1, '__esModule', {
|
|
20
|
+
value: true
|
|
21
|
+
});
|
|
22
|
+
};
|
|
23
|
+
})();
|
|
24
|
+
var __webpack_exports__ = {};
|
|
25
|
+
__webpack_require__.r(__webpack_exports__);
|
|
26
|
+
__webpack_require__.d(__webpack_exports__, {
|
|
27
|
+
PROXY_PID_FILE: ()=>PROXY_PID_FILE,
|
|
28
|
+
PROXY_ENDPOINT_FILE: ()=>PROXY_ENDPOINT_FILE
|
|
29
|
+
});
|
|
30
|
+
const external_node_os_namespaceObject = require("node:os");
|
|
31
|
+
const external_node_path_namespaceObject = require("node:path");
|
|
32
|
+
const PROXY_ENDPOINT_FILE = (0, external_node_path_namespaceObject.join)((0, external_node_os_namespaceObject.tmpdir)(), 'midscene-cdp-proxy-endpoint');
|
|
33
|
+
const PROXY_PID_FILE = (0, external_node_path_namespaceObject.join)((0, external_node_os_namespaceObject.tmpdir)(), 'midscene-cdp-proxy-pid');
|
|
34
|
+
exports.PROXY_ENDPOINT_FILE = __webpack_exports__.PROXY_ENDPOINT_FILE;
|
|
35
|
+
exports.PROXY_PID_FILE = __webpack_exports__.PROXY_PID_FILE;
|
|
36
|
+
for(var __rspack_i in __webpack_exports__)if (-1 === [
|
|
37
|
+
"PROXY_ENDPOINT_FILE",
|
|
38
|
+
"PROXY_PID_FILE"
|
|
39
|
+
].indexOf(__rspack_i)) exports[__rspack_i] = __webpack_exports__[__rspack_i];
|
|
40
|
+
Object.defineProperty(exports, '__esModule', {
|
|
41
|
+
value: true
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
//# sourceMappingURL=cdp-proxy-constants.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cdp-proxy-constants.js","sources":["webpack/runtime/define_property_getters","webpack/runtime/has_own_property","webpack/runtime/make_namespace_object","../../src/cdp-proxy-constants.ts"],"sourcesContent":["__webpack_require__.d = (exports, definition) => {\n\tfor(var key in definition) {\n if(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {\n Object.defineProperty(exports, key, { enumerable: true, get: definition[key] });\n }\n }\n};","__webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))","// define __esModule on exports\n__webpack_require__.r = (exports) => {\n\tif(typeof Symbol !== 'undefined' && Symbol.toStringTag) {\n\t\tObject.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });\n\t}\n\tObject.defineProperty(exports, '__esModule', { value: true });\n};","/**\n * Shared constants for CDP proxy discovery between cdp-proxy.ts and mcp-tools-cdp.ts.\n */\nimport { tmpdir } from 'node:os';\nimport { join } from 'node:path';\n\nexport const PROXY_ENDPOINT_FILE = join(\n tmpdir(),\n 'midscene-cdp-proxy-endpoint',\n);\nexport const PROXY_PID_FILE = join(tmpdir(), 'midscene-cdp-proxy-pid');\n"],"names":["__webpack_require__","definition","key","Object","obj","prop","Symbol","PROXY_ENDPOINT_FILE","join","tmpdir","PROXY_PID_FILE"],"mappings":";;;IAAAA,oBAAoB,CAAC,GAAG,CAAC,UAASC;QACjC,IAAI,IAAIC,OAAOD,WACR,IAAGD,oBAAoB,CAAC,CAACC,YAAYC,QAAQ,CAACF,oBAAoB,CAAC,CAAC,UAASE,MACzEC,OAAO,cAAc,CAAC,UAASD,KAAK;YAAE,YAAY;YAAM,KAAKD,UAAU,CAACC,IAAI;QAAC;IAGzF;;;ICNAF,oBAAoB,CAAC,GAAG,CAACI,KAAKC,OAAUF,OAAO,SAAS,CAAC,cAAc,CAAC,IAAI,CAACC,KAAKC;;;ICClFL,oBAAoB,CAAC,GAAG,CAAC;QACxB,IAAG,AAAkB,eAAlB,OAAOM,UAA0BA,OAAO,WAAW,EACrDH,OAAO,cAAc,CAAC,UAASG,OAAO,WAAW,EAAE;YAAE,OAAO;QAAS;QAEtEH,OAAO,cAAc,CAAC,UAAS,cAAc;YAAE,OAAO;QAAK;IAC5D;;;;;;;;;;ACAO,MAAMI,sBAAsBC,AAAAA,IAAAA,mCAAAA,IAAAA,AAAAA,EACjCC,AAAAA,IAAAA,iCAAAA,MAAAA,AAAAA,KACA;AAEK,MAAMC,iBAAiBF,AAAAA,IAAAA,mCAAAA,IAAAA,AAAAA,EAAKC,AAAAA,IAAAA,iCAAAA,MAAAA,AAAAA,KAAU"}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __webpack_require__ = {};
|
|
3
|
+
(()=>{
|
|
4
|
+
__webpack_require__.n = (module)=>{
|
|
5
|
+
var getter = module && module.__esModule ? ()=>module['default'] : ()=>module;
|
|
6
|
+
__webpack_require__.d(getter, {
|
|
7
|
+
a: getter
|
|
8
|
+
});
|
|
9
|
+
return getter;
|
|
10
|
+
};
|
|
11
|
+
})();
|
|
12
|
+
(()=>{
|
|
13
|
+
__webpack_require__.d = (exports1, definition)=>{
|
|
14
|
+
for(var key in definition)if (__webpack_require__.o(definition, key) && !__webpack_require__.o(exports1, key)) Object.defineProperty(exports1, key, {
|
|
15
|
+
enumerable: true,
|
|
16
|
+
get: definition[key]
|
|
17
|
+
});
|
|
18
|
+
};
|
|
19
|
+
})();
|
|
20
|
+
(()=>{
|
|
21
|
+
__webpack_require__.o = (obj, prop)=>Object.prototype.hasOwnProperty.call(obj, prop);
|
|
22
|
+
})();
|
|
23
|
+
var __webpack_exports__ = {};
|
|
24
|
+
const external_node_fs_namespaceObject = require("node:fs");
|
|
25
|
+
const external_node_http_namespaceObject = require("node:http");
|
|
26
|
+
const external_ws_namespaceObject = require("ws");
|
|
27
|
+
var external_ws_default = /*#__PURE__*/ __webpack_require__.n(external_ws_namespaceObject);
|
|
28
|
+
const external_cdp_proxy_constants_js_namespaceObject = require("./cdp-proxy-constants.js");
|
|
29
|
+
const IDLE_TIMEOUT_MS = 300000;
|
|
30
|
+
const chromeEndpoint = process.argv[2];
|
|
31
|
+
if (!chromeEndpoint) {
|
|
32
|
+
process.stderr.write('Usage: node cdp-proxy.js <chrome-ws-endpoint>\n');
|
|
33
|
+
process.exit(1);
|
|
34
|
+
}
|
|
35
|
+
function cleanupIfOwned() {
|
|
36
|
+
try {
|
|
37
|
+
if ((0, external_node_fs_namespaceObject.existsSync)(external_cdp_proxy_constants_js_namespaceObject.PROXY_PID_FILE)) {
|
|
38
|
+
const pid = Number((0, external_node_fs_namespaceObject.readFileSync)(external_cdp_proxy_constants_js_namespaceObject.PROXY_PID_FILE, 'utf-8').trim());
|
|
39
|
+
if (pid !== process.pid) return;
|
|
40
|
+
}
|
|
41
|
+
} catch {}
|
|
42
|
+
try {
|
|
43
|
+
if ((0, external_node_fs_namespaceObject.existsSync)(external_cdp_proxy_constants_js_namespaceObject.PROXY_ENDPOINT_FILE)) (0, external_node_fs_namespaceObject.unlinkSync)(external_cdp_proxy_constants_js_namespaceObject.PROXY_ENDPOINT_FILE);
|
|
44
|
+
} catch {}
|
|
45
|
+
try {
|
|
46
|
+
if ((0, external_node_fs_namespaceObject.existsSync)(external_cdp_proxy_constants_js_namespaceObject.PROXY_PID_FILE)) (0, external_node_fs_namespaceObject.unlinkSync)(external_cdp_proxy_constants_js_namespaceObject.PROXY_PID_FILE);
|
|
47
|
+
} catch {}
|
|
48
|
+
}
|
|
49
|
+
function shutdown(reason) {
|
|
50
|
+
process.stderr.write(`[cdp-proxy] shutting down: ${reason}\n`);
|
|
51
|
+
cleanupIfOwned();
|
|
52
|
+
process.exit(0);
|
|
53
|
+
}
|
|
54
|
+
process.on('SIGTERM', ()=>shutdown('SIGTERM'));
|
|
55
|
+
process.on('SIGINT', ()=>shutdown('SIGINT'));
|
|
56
|
+
process.on('uncaughtException', (e)=>shutdown(`uncaught: ${e.message}`));
|
|
57
|
+
let idleTimer = null;
|
|
58
|
+
function resetIdleTimer() {
|
|
59
|
+
if (idleTimer) clearTimeout(idleTimer);
|
|
60
|
+
idleTimer = setTimeout(()=>shutdown('idle timeout (5min)'), IDLE_TIMEOUT_MS);
|
|
61
|
+
}
|
|
62
|
+
resetIdleTimer();
|
|
63
|
+
const upstream = new (external_ws_default())(chromeEndpoint);
|
|
64
|
+
const clients = new Set();
|
|
65
|
+
upstream.on('error', (err)=>shutdown(`upstream error: ${err.message}`));
|
|
66
|
+
upstream.on('close', ()=>shutdown('upstream closed'));
|
|
67
|
+
upstream.on('message', (data, isBinary)=>{
|
|
68
|
+
resetIdleTimer();
|
|
69
|
+
for (const client of clients)if (client.readyState === external_ws_default().OPEN) client.send(data, {
|
|
70
|
+
binary: isBinary
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
const httpServer = (0, external_node_http_namespaceObject.createServer)((_req, res)=>{
|
|
74
|
+
res.writeHead(404);
|
|
75
|
+
res.end();
|
|
76
|
+
});
|
|
77
|
+
const wss = new external_ws_namespaceObject.WebSocketServer({
|
|
78
|
+
server: httpServer
|
|
79
|
+
});
|
|
80
|
+
wss.on('connection', (clientWs)=>{
|
|
81
|
+
clients.add(clientWs);
|
|
82
|
+
resetIdleTimer();
|
|
83
|
+
clientWs.on('message', (data, isBinary)=>{
|
|
84
|
+
resetIdleTimer();
|
|
85
|
+
if (upstream.readyState === external_ws_default().OPEN) upstream.send(data, {
|
|
86
|
+
binary: isBinary
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
clientWs.on('close', ()=>clients.delete(clientWs));
|
|
90
|
+
clientWs.on('error', ()=>clients.delete(clientWs));
|
|
91
|
+
});
|
|
92
|
+
upstream.on('open', ()=>{
|
|
93
|
+
if ((0, external_node_fs_namespaceObject.existsSync)(external_cdp_proxy_constants_js_namespaceObject.PROXY_PID_FILE)) try {
|
|
94
|
+
const existingPid = Number((0, external_node_fs_namespaceObject.readFileSync)(external_cdp_proxy_constants_js_namespaceObject.PROXY_PID_FILE, 'utf-8').trim());
|
|
95
|
+
if (existingPid !== process.pid) try {
|
|
96
|
+
process.kill(existingPid, 0);
|
|
97
|
+
process.exit(0);
|
|
98
|
+
} catch {}
|
|
99
|
+
} catch {}
|
|
100
|
+
httpServer.listen(0, '127.0.0.1', ()=>{
|
|
101
|
+
const addr = httpServer.address();
|
|
102
|
+
if (!addr || 'string' == typeof addr) return void shutdown('failed to get server address');
|
|
103
|
+
const proxyEndpoint = `ws://127.0.0.1:${addr.port}/devtools/browser`;
|
|
104
|
+
(0, external_node_fs_namespaceObject.writeFileSync)(external_cdp_proxy_constants_js_namespaceObject.PROXY_ENDPOINT_FILE, proxyEndpoint);
|
|
105
|
+
(0, external_node_fs_namespaceObject.writeFileSync)(external_cdp_proxy_constants_js_namespaceObject.PROXY_PID_FILE, String(process.pid));
|
|
106
|
+
process.stdout.write(`${JSON.stringify({
|
|
107
|
+
endpoint: proxyEndpoint
|
|
108
|
+
})}\n`);
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
for(var __rspack_i in __webpack_exports__)exports[__rspack_i] = __webpack_exports__[__rspack_i];
|
|
112
|
+
Object.defineProperty(exports, '__esModule', {
|
|
113
|
+
value: true
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
//# sourceMappingURL=cdp-proxy.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cdp-proxy.js","sources":["webpack/runtime/compat_get_default_export","webpack/runtime/define_property_getters","webpack/runtime/has_own_property","../../src/cdp-proxy.ts"],"sourcesContent":["// getDefaultExport function for compatibility with non-ESM modules\n__webpack_require__.n = (module) => {\n\tvar getter = module && module.__esModule ?\n\t\t() => (module['default']) :\n\t\t() => (module);\n\t__webpack_require__.d(getter, { a: getter });\n\treturn getter;\n};\n","__webpack_require__.d = (exports, definition) => {\n\tfor(var key in definition) {\n if(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {\n Object.defineProperty(exports, key, { enumerable: true, get: definition[key] });\n }\n }\n};","__webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))","/**\n * CDP WebSocket Proxy — standalone process.\n *\n * Holds a single persistent WebSocket connection to Chrome's CDP endpoint and\n * exposes a local WebSocket server. Midscene CLI processes connect to the proxy\n * instead of Chrome directly, so Chrome's \"Allow remote debugging\" permission\n * popup only fires once (when the proxy connects).\n *\n * Exit conditions:\n * 1. Upstream Chrome connection closes or errors.\n * 2. No downstream client message for IDLE_TIMEOUT_MS (default 5 min).\n * 3. SIGTERM / SIGINT.\n *\n * Usage (spawned by mcp-tools-cdp.ts):\n * node cdp-proxy.js <chrome-ws-endpoint>\n *\n * On startup, prints the proxy endpoint to stdout as a single JSON line:\n * {\"endpoint\":\"ws://127.0.0.1:<port>/devtools/browser\"}\n * and writes the same endpoint to PROXY_ENDPOINT_FILE.\n */\n\nimport { existsSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs';\nimport { createServer } from 'node:http';\nimport WebSocket, { WebSocketServer } from 'ws';\nimport { PROXY_ENDPOINT_FILE, PROXY_PID_FILE } from './cdp-proxy-constants';\n\n// ---------------------------------------------------------------------------\n// Config\n// ---------------------------------------------------------------------------\n\nconst IDLE_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes\n\nconst chromeEndpoint = process.argv[2];\nif (!chromeEndpoint) {\n process.stderr.write('Usage: node cdp-proxy.js <chrome-ws-endpoint>\\n');\n process.exit(1);\n}\n\n// ---------------------------------------------------------------------------\n// Cleanup\n// ---------------------------------------------------------------------------\n\nfunction cleanupIfOwned() {\n try {\n if (existsSync(PROXY_PID_FILE)) {\n const pid = Number(readFileSync(PROXY_PID_FILE, 'utf-8').trim());\n if (pid !== process.pid) return;\n }\n } catch {}\n try {\n if (existsSync(PROXY_ENDPOINT_FILE)) unlinkSync(PROXY_ENDPOINT_FILE);\n } catch {}\n try {\n if (existsSync(PROXY_PID_FILE)) unlinkSync(PROXY_PID_FILE);\n } catch {}\n}\n\nfunction shutdown(reason: string) {\n process.stderr.write(`[cdp-proxy] shutting down: ${reason}\\n`);\n cleanupIfOwned();\n process.exit(0);\n}\n\nprocess.on('SIGTERM', () => shutdown('SIGTERM'));\nprocess.on('SIGINT', () => shutdown('SIGINT'));\nprocess.on('uncaughtException', (e) => shutdown(`uncaught: ${e.message}`));\n\n// ---------------------------------------------------------------------------\n// Idle timer\n// ---------------------------------------------------------------------------\n\nlet idleTimer: ReturnType<typeof setTimeout> | null = null;\n\nfunction resetIdleTimer() {\n if (idleTimer) clearTimeout(idleTimer);\n idleTimer = setTimeout(\n () => shutdown('idle timeout (5min)'),\n IDLE_TIMEOUT_MS,\n );\n}\n\nresetIdleTimer();\n\n// ---------------------------------------------------------------------------\n// Upstream: connect to Chrome\n// ---------------------------------------------------------------------------\n\nconst upstream = new WebSocket(chromeEndpoint);\nconst clients = new Set<WebSocket>();\n\nupstream.on('error', (err) => shutdown(`upstream error: ${err.message}`));\nupstream.on('close', () => shutdown('upstream closed'));\n\n// Forward upstream messages to all downstream clients\nupstream.on('message', (data, isBinary) => {\n resetIdleTimer();\n for (const client of clients) {\n if (client.readyState === WebSocket.OPEN) {\n client.send(data, { binary: isBinary });\n }\n }\n});\n\n// ---------------------------------------------------------------------------\n// Downstream: local WebSocket server\n// ---------------------------------------------------------------------------\n\nconst httpServer = createServer((_req, res) => {\n res.writeHead(404);\n res.end();\n});\n\nconst wss = new WebSocketServer({ server: httpServer });\n\nwss.on('connection', (clientWs) => {\n clients.add(clientWs);\n resetIdleTimer();\n\n // Forward downstream messages to upstream\n clientWs.on('message', (data, isBinary) => {\n resetIdleTimer();\n if (upstream.readyState === WebSocket.OPEN) {\n upstream.send(data, { binary: isBinary });\n }\n });\n\n clientWs.on('close', () => clients.delete(clientWs));\n clientWs.on('error', () => clients.delete(clientWs));\n});\n\n// ---------------------------------------------------------------------------\n// Start\n// ---------------------------------------------------------------------------\n\nupstream.on('open', () => {\n // Check for duplicate proxy\n if (existsSync(PROXY_PID_FILE)) {\n try {\n const existingPid = Number(readFileSync(PROXY_PID_FILE, 'utf-8').trim());\n if (existingPid !== process.pid) {\n try {\n process.kill(existingPid, 0);\n process.exit(0); // another proxy is alive\n } catch {\n // dead — we take over\n }\n }\n } catch {}\n }\n\n httpServer.listen(0, '127.0.0.1', () => {\n const addr = httpServer.address();\n if (!addr || typeof addr === 'string') {\n shutdown('failed to get server address');\n return;\n }\n\n const proxyEndpoint = `ws://127.0.0.1:${addr.port}/devtools/browser`;\n\n writeFileSync(PROXY_ENDPOINT_FILE, proxyEndpoint);\n writeFileSync(PROXY_PID_FILE, String(process.pid));\n\n process.stdout.write(`${JSON.stringify({ endpoint: proxyEndpoint })}\\n`);\n });\n});\n"],"names":["__webpack_require__","module","getter","definition","key","Object","obj","prop","IDLE_TIMEOUT_MS","chromeEndpoint","process","cleanupIfOwned","existsSync","PROXY_PID_FILE","pid","Number","readFileSync","PROXY_ENDPOINT_FILE","unlinkSync","shutdown","reason","e","idleTimer","resetIdleTimer","clearTimeout","setTimeout","upstream","WebSocket","clients","Set","err","data","isBinary","client","httpServer","createServer","_req","res","wss","WebSocketServer","clientWs","existingPid","addr","proxyEndpoint","writeFileSync","String","JSON"],"mappings":"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"}
|
package/dist/lib/cli.js
CHANGED
|
@@ -63,7 +63,7 @@ tools = isBridge ? new external_mcp_tools_js_namespaceObject.WebMidsceneTools()
|
|
|
63
63
|
(0, cli_namespaceObject.runToolsCLI)(tools, 'midscene-web', {
|
|
64
64
|
stripPrefix: 'web_',
|
|
65
65
|
argv,
|
|
66
|
-
version: "1.
|
|
66
|
+
version: "1.6.0"
|
|
67
67
|
}).catch((e)=>{
|
|
68
68
|
if (!(e instanceof cli_namespaceObject.CLIError)) console.error(e);
|
|
69
69
|
process.exit(e instanceof cli_namespaceObject.CLIError ? e.exitCode : 1);
|
package/dist/lib/mcp-server.js
CHANGED
|
@@ -37,7 +37,7 @@ class WebMCPServer extends mcp_namespaceObject.BaseMCPServer {
|
|
|
37
37
|
constructor(toolsManager){
|
|
38
38
|
super({
|
|
39
39
|
name: '@midscene/web-bridge-mcp',
|
|
40
|
-
version: "1.
|
|
40
|
+
version: "1.6.0",
|
|
41
41
|
description: 'Control the browser using natural language commands'
|
|
42
42
|
}, toolsManager);
|
|
43
43
|
}
|
|
@@ -35,10 +35,14 @@ __webpack_require__.r(__webpack_exports__);
|
|
|
35
35
|
__webpack_require__.d(__webpack_exports__, {
|
|
36
36
|
WebCdpMidsceneTools: ()=>WebCdpMidsceneTools
|
|
37
37
|
});
|
|
38
|
+
const external_node_child_process_namespaceObject = require("node:child_process");
|
|
39
|
+
const external_node_fs_namespaceObject = require("node:fs");
|
|
40
|
+
const external_node_path_namespaceObject = require("node:path");
|
|
38
41
|
const core_namespaceObject = require("@midscene/core");
|
|
39
42
|
const mcp_namespaceObject = require("@midscene/shared/mcp");
|
|
40
43
|
const external_puppeteer_core_namespaceObject = require("puppeteer-core");
|
|
41
44
|
var external_puppeteer_core_default = /*#__PURE__*/ __webpack_require__.n(external_puppeteer_core_namespaceObject);
|
|
45
|
+
const external_cdp_proxy_constants_js_namespaceObject = require("./cdp-proxy-constants.js");
|
|
42
46
|
const index_js_namespaceObject = require("./puppeteer/index.js");
|
|
43
47
|
const external_static_index_js_namespaceObject = require("./static/index.js");
|
|
44
48
|
function _define_property(obj, key, value) {
|
|
@@ -51,6 +55,90 @@ function _define_property(obj, key, value) {
|
|
|
51
55
|
else obj[key] = value;
|
|
52
56
|
return obj;
|
|
53
57
|
}
|
|
58
|
+
function isProxyAlive() {
|
|
59
|
+
if (!(0, external_node_fs_namespaceObject.existsSync)(external_cdp_proxy_constants_js_namespaceObject.PROXY_PID_FILE)) return false;
|
|
60
|
+
try {
|
|
61
|
+
const pid = Number((0, external_node_fs_namespaceObject.readFileSync)(external_cdp_proxy_constants_js_namespaceObject.PROXY_PID_FILE, 'utf-8').trim());
|
|
62
|
+
process.kill(pid, 0);
|
|
63
|
+
return true;
|
|
64
|
+
} catch {
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
function readProxyEndpoint() {
|
|
69
|
+
if (!(0, external_node_fs_namespaceObject.existsSync)(external_cdp_proxy_constants_js_namespaceObject.PROXY_ENDPOINT_FILE)) return null;
|
|
70
|
+
try {
|
|
71
|
+
return (0, external_node_fs_namespaceObject.readFileSync)(external_cdp_proxy_constants_js_namespaceObject.PROXY_ENDPOINT_FILE, 'utf-8').trim();
|
|
72
|
+
} catch {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
function spawnProxy(chromeEndpoint) {
|
|
77
|
+
return new Promise((resolve, reject)=>{
|
|
78
|
+
const proxyScript = (0, external_node_path_namespaceObject.join)(__dirname, 'cdp-proxy.js');
|
|
79
|
+
const proc = (0, external_node_child_process_namespaceObject.spawn)(process.execPath, [
|
|
80
|
+
proxyScript,
|
|
81
|
+
chromeEndpoint
|
|
82
|
+
], {
|
|
83
|
+
detached: true,
|
|
84
|
+
stdio: [
|
|
85
|
+
'ignore',
|
|
86
|
+
'pipe',
|
|
87
|
+
'ignore'
|
|
88
|
+
]
|
|
89
|
+
});
|
|
90
|
+
proc.unref();
|
|
91
|
+
let output = '';
|
|
92
|
+
let settled = false;
|
|
93
|
+
const timer = setTimeout(()=>{
|
|
94
|
+
if (!settled) {
|
|
95
|
+
settled = true;
|
|
96
|
+
reject(new Error('Proxy startup timeout (10s)'));
|
|
97
|
+
}
|
|
98
|
+
}, 10000);
|
|
99
|
+
const onData = (chunk)=>{
|
|
100
|
+
output += chunk.toString();
|
|
101
|
+
const lines = output.split('\n');
|
|
102
|
+
for (const line of lines)if (line.trim()) try {
|
|
103
|
+
const parsed = JSON.parse(line);
|
|
104
|
+
if (parsed.endpoint && !settled) {
|
|
105
|
+
settled = true;
|
|
106
|
+
clearTimeout(timer);
|
|
107
|
+
proc.stdout.removeListener('data', onData);
|
|
108
|
+
resolve(parsed.endpoint);
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
} catch {}
|
|
112
|
+
};
|
|
113
|
+
proc.stdout.on('data', onData);
|
|
114
|
+
proc.on('error', (err)=>{
|
|
115
|
+
if (!settled) {
|
|
116
|
+
settled = true;
|
|
117
|
+
clearTimeout(timer);
|
|
118
|
+
reject(new Error(`Failed to spawn proxy: ${err.message}`));
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
proc.on('exit', (code)=>{
|
|
122
|
+
if (!settled) {
|
|
123
|
+
settled = true;
|
|
124
|
+
clearTimeout(timer);
|
|
125
|
+
reject(new Error(`Proxy exited with code ${code} before ready`));
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
async function getProxyEndpoint(chromeEndpoint) {
|
|
131
|
+
if (isProxyAlive()) {
|
|
132
|
+
const endpoint = readProxyEndpoint();
|
|
133
|
+
if (endpoint) return endpoint;
|
|
134
|
+
}
|
|
135
|
+
try {
|
|
136
|
+
return await spawnProxy(chromeEndpoint);
|
|
137
|
+
} catch (err) {
|
|
138
|
+
console.warn(`[cdp] proxy failed, falling back to direct connection: ${err}`);
|
|
139
|
+
return chromeEndpoint;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
54
142
|
class WebCdpMidsceneTools extends mcp_namespaceObject.BaseMidsceneTools {
|
|
55
143
|
createTemporaryDevice() {
|
|
56
144
|
return new external_static_index_js_namespaceObject.StaticPage({
|
|
@@ -72,21 +160,32 @@ class WebCdpMidsceneTools extends mcp_namespaceObject.BaseMidsceneTools {
|
|
|
72
160
|
this.agent = void 0;
|
|
73
161
|
}
|
|
74
162
|
if (this.agent) return this.agent;
|
|
75
|
-
if (!this.activeBrowser)
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
163
|
+
if (!this.activeBrowser) {
|
|
164
|
+
const endpoint = await getProxyEndpoint(this.cdpEndpoint);
|
|
165
|
+
this.activeBrowser = await external_puppeteer_core_default().connect({
|
|
166
|
+
browserWSEndpoint: endpoint,
|
|
167
|
+
defaultViewport: null
|
|
168
|
+
});
|
|
169
|
+
}
|
|
79
170
|
const browser = this.activeBrowser;
|
|
80
171
|
const pages = await browser.pages();
|
|
172
|
+
const webPages = pages.filter((p)=>/^https?:\/\//.test(p.url()));
|
|
81
173
|
let page;
|
|
82
|
-
if (navigateToUrl) {
|
|
83
|
-
page =
|
|
174
|
+
if (navigateToUrl) if (webPages.length > 0) {
|
|
175
|
+
page = webPages[webPages.length - 1];
|
|
176
|
+
await page.bringToFront();
|
|
84
177
|
await page.goto(navigateToUrl, {
|
|
85
178
|
timeout: 30000,
|
|
86
179
|
waitUntil: 'domcontentloaded'
|
|
87
180
|
});
|
|
88
181
|
} else {
|
|
89
|
-
|
|
182
|
+
page = await browser.newPage();
|
|
183
|
+
await page.goto(navigateToUrl, {
|
|
184
|
+
timeout: 30000,
|
|
185
|
+
waitUntil: 'domcontentloaded'
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
else {
|
|
90
189
|
page = webPages.length > 0 ? webPages[webPages.length - 1] : pages[pages.length - 1] || await browser.newPage();
|
|
91
190
|
await page.bringToFront();
|
|
92
191
|
}
|
|
@@ -113,7 +212,9 @@ class WebCdpMidsceneTools extends mcp_namespaceObject.BaseMidsceneTools {
|
|
|
113
212
|
if (this.agent) {
|
|
114
213
|
try {
|
|
115
214
|
await this.agent.destroy?.();
|
|
116
|
-
} catch
|
|
215
|
+
} catch (e) {
|
|
216
|
+
console.debug('Failed to destroy agent during connect:', e);
|
|
217
|
+
}
|
|
117
218
|
this.agent = void 0;
|
|
118
219
|
}
|
|
119
220
|
this.agent = await this.ensureAgent(url);
|
|
@@ -138,7 +239,9 @@ class WebCdpMidsceneTools extends mcp_namespaceObject.BaseMidsceneTools {
|
|
|
138
239
|
if (this.agent) {
|
|
139
240
|
try {
|
|
140
241
|
await this.agent.destroy?.();
|
|
141
|
-
} catch
|
|
242
|
+
} catch (e) {
|
|
243
|
+
console.debug('Failed to destroy agent during disconnect:', e);
|
|
244
|
+
}
|
|
142
245
|
this.agent = void 0;
|
|
143
246
|
}
|
|
144
247
|
if (this.activeBrowser) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mcp-tools-cdp.js","sources":["webpack/runtime/compat_get_default_export","webpack/runtime/define_property_getters","webpack/runtime/has_own_property","webpack/runtime/make_namespace_object","../../src/mcp-tools-cdp.ts"],"sourcesContent":["// getDefaultExport function for compatibility with non-ESM modules\n__webpack_require__.n = (module) => {\n\tvar getter = module && module.__esModule ?\n\t\t() => (module['default']) :\n\t\t() => (module);\n\t__webpack_require__.d(getter, { a: getter });\n\treturn getter;\n};\n","__webpack_require__.d = (exports, definition) => {\n\tfor(var key in definition) {\n if(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {\n Object.defineProperty(exports, key, { enumerable: true, get: definition[key] });\n }\n }\n};","__webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))","// define __esModule on exports\n__webpack_require__.r = (exports) => {\n\tif(typeof Symbol !== 'undefined' && Symbol.toStringTag) {\n\t\tObject.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });\n\t}\n\tObject.defineProperty(exports, '__esModule', { value: true });\n};","import { ScreenshotItem, z } from '@midscene/core';\nimport { BaseMidsceneTools, type ToolDefinition } from '@midscene/shared/mcp';\nimport type { Page as PuppeteerPage } from 'puppeteer';\nimport puppeteer from 'puppeteer-core';\nimport type { Browser, Page } from 'puppeteer-core';\nimport { PuppeteerAgent } from './puppeteer';\nimport { StaticPage } from './static';\n\n/**\n * Tools manager for Web CDP-mode MCP.\n * Connects to an existing Chrome browser via CDP (Chrome DevTools Protocol) endpoint.\n * Unlike WebPuppeteerMidsceneTools which launches its own Chrome, this connects\n * to a browser that is already running with remote debugging enabled.\n */\nexport class WebCdpMidsceneTools extends BaseMidsceneTools<PuppeteerAgent> {\n private cdpEndpoint: string;\n private activeBrowser: Browser | null = null;\n\n constructor(cdpEndpoint: string) {\n super();\n this.cdpEndpoint = cdpEndpoint;\n }\n\n protected createTemporaryDevice() {\n return new StaticPage({\n screenshot: ScreenshotItem.create('', Date.now()),\n shotSize: { width: 1920, height: 1080 },\n shrunkShotToLogicalRatio: 1,\n });\n }\n\n protected async ensureAgent(navigateToUrl?: string): Promise<PuppeteerAgent> {\n // Re-init if URL provided\n if (this.agent && navigateToUrl) {\n try {\n await this.agent?.destroy?.();\n } catch (error) {\n console.debug('Failed to destroy agent during re-init:', error);\n }\n this.agent = undefined;\n }\n\n if (this.agent) return this.agent;\n\n // Connect to the existing browser via CDP endpoint\n if (!this.activeBrowser) {\n this.activeBrowser = await puppeteer.connect({\n browserWSEndpoint: this.cdpEndpoint,\n defaultViewport: null,\n });\n }\n\n const browser = this.activeBrowser;\n const pages = await browser.pages();\n let page: Page;\n\n if (navigateToUrl) {\n page = await browser.newPage();\n await page.goto(navigateToUrl, {\n timeout: 30000,\n waitUntil: 'domcontentloaded',\n });\n } else {\n // Reuse the last web page\n const webPages = pages.filter((p) => /^https?:\\/\\//.test(p.url()));\n page =\n webPages.length > 0\n ? webPages[webPages.length - 1]\n : pages[pages.length - 1] || (await browser.newPage());\n\n await page.bringToFront();\n }\n\n this.agent = new PuppeteerAgent(page as unknown as PuppeteerPage);\n return this.agent;\n }\n\n public async destroy(): Promise<void> {\n await super.destroy();\n if (this.activeBrowser) {\n this.activeBrowser.disconnect();\n this.activeBrowser = null;\n }\n }\n\n protected preparePlatformTools(): ToolDefinition[] {\n return [\n {\n name: 'web_connect',\n description:\n 'Connect to a web page via CDP. Opens a new tab with the given URL, or reuses the current page.',\n schema: {\n url: z\n .string()\n .url()\n .optional()\n .describe('URL to open in new tab (omit to use current page)'),\n },\n handler: async (args) => {\n const { url } = args as { url?: string };\n\n // Destroy existing agent\n if (this.agent) {\n try {\n await this.agent.destroy?.();\n } catch {}\n this.agent = undefined;\n }\n\n this.agent = await this.ensureAgent(url);\n\n const screenshot = await this.agent.page?.screenshotBase64();\n const label = url ?? 'current page';\n\n return {\n content: [\n { type: 'text', text: `Connected via CDP to: ${label}` },\n ...(screenshot ? this.buildScreenshotContent(screenshot) : []),\n ],\n };\n },\n },\n {\n name: 'web_disconnect',\n description:\n 'Disconnect from current web page. The browser stays running (managed externally).',\n schema: {},\n handler: async () => {\n if (this.agent) {\n try {\n await this.agent.destroy?.();\n } catch {}\n this.agent = undefined;\n }\n if (this.activeBrowser) {\n this.activeBrowser.disconnect();\n this.activeBrowser = null;\n }\n return this.buildTextResult(\n 'Disconnected from web page (browser still running externally)',\n );\n },\n },\n ];\n }\n}\n"],"names":["__webpack_require__","module","getter","definition","key","Object","obj","prop","Symbol","WebCdpMidsceneTools","BaseMidsceneTools","StaticPage","ScreenshotItem","Date","navigateToUrl","error","console","undefined","puppeteer","browser","pages","page","webPages","p","PuppeteerAgent","z","args","url","screenshot","label","cdpEndpoint"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"mcp-tools-cdp.js","sources":["webpack/runtime/compat_get_default_export","webpack/runtime/define_property_getters","webpack/runtime/has_own_property","webpack/runtime/make_namespace_object","../../src/mcp-tools-cdp.ts"],"sourcesContent":["// getDefaultExport function for compatibility with non-ESM modules\n__webpack_require__.n = (module) => {\n\tvar getter = module && module.__esModule ?\n\t\t() => (module['default']) :\n\t\t() => (module);\n\t__webpack_require__.d(getter, { a: getter });\n\treturn getter;\n};\n","__webpack_require__.d = (exports, definition) => {\n\tfor(var key in definition) {\n if(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {\n Object.defineProperty(exports, key, { enumerable: true, get: definition[key] });\n }\n }\n};","__webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))","// define __esModule on exports\n__webpack_require__.r = (exports) => {\n\tif(typeof Symbol !== 'undefined' && Symbol.toStringTag) {\n\t\tObject.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });\n\t}\n\tObject.defineProperty(exports, '__esModule', { value: true });\n};","import { spawn } from 'node:child_process';\nimport { existsSync, readFileSync } from 'node:fs';\nimport { join } from 'node:path';\nimport { ScreenshotItem, z } from '@midscene/core';\nimport { BaseMidsceneTools, type ToolDefinition } from '@midscene/shared/mcp';\nimport type { Page as PuppeteerPage } from 'puppeteer';\nimport puppeteer from 'puppeteer-core';\nimport type { Browser, Page } from 'puppeteer-core';\nimport { PROXY_ENDPOINT_FILE, PROXY_PID_FILE } from './cdp-proxy-constants';\nimport { PuppeteerAgent } from './puppeteer';\nimport { StaticPage } from './static';\n\n/**\n * Check if a previously spawned proxy process is still alive.\n */\nfunction isProxyAlive(): boolean {\n if (!existsSync(PROXY_PID_FILE)) return false;\n try {\n const pid = Number(readFileSync(PROXY_PID_FILE, 'utf-8').trim());\n process.kill(pid, 0); // signal 0 = existence check\n return true;\n } catch {\n return false;\n }\n}\n\n/**\n * Read the proxy endpoint written by cdp-proxy.ts.\n */\nfunction readProxyEndpoint(): string | null {\n if (!existsSync(PROXY_ENDPOINT_FILE)) return null;\n try {\n return readFileSync(PROXY_ENDPOINT_FILE, 'utf-8').trim();\n } catch {\n return null;\n }\n}\n\n/**\n * Spawn the CDP proxy process and wait for it to print the endpoint.\n */\nfunction spawnProxy(chromeEndpoint: string): Promise<string> {\n return new Promise((resolve, reject) => {\n const proxyScript = join(__dirname, 'cdp-proxy.js');\n const proc = spawn(process.execPath, [proxyScript, chromeEndpoint], {\n detached: true,\n stdio: ['ignore', 'pipe', 'ignore'],\n });\n proc.unref();\n\n let output = '';\n let settled = false;\n const timer = setTimeout(() => {\n if (!settled) {\n settled = true;\n reject(new Error('Proxy startup timeout (10s)'));\n }\n }, 10000);\n\n const onData = (chunk: Buffer) => {\n output += chunk.toString();\n const lines = output.split('\\n');\n for (const line of lines) {\n if (!line.trim()) continue;\n try {\n const parsed = JSON.parse(line);\n if (parsed.endpoint && !settled) {\n settled = true;\n clearTimeout(timer);\n proc.stdout!.removeListener('data', onData);\n resolve(parsed.endpoint);\n return;\n }\n } catch {\n // stdout may contain non-JSON lines during startup — skip them\n }\n }\n };\n proc.stdout!.on('data', onData);\n\n proc.on('error', (err) => {\n if (!settled) {\n settled = true;\n clearTimeout(timer);\n reject(new Error(`Failed to spawn proxy: ${err.message}`));\n }\n });\n proc.on('exit', (code) => {\n if (!settled) {\n settled = true;\n clearTimeout(timer);\n reject(new Error(`Proxy exited with code ${code} before ready`));\n }\n });\n });\n}\n\n/**\n * Get the proxy endpoint, spawning the proxy if needed.\n * Falls back to direct connection if proxy cannot be started.\n */\nasync function getProxyEndpoint(chromeEndpoint: string): Promise<string> {\n // If proxy is alive and endpoint file exists, reuse it\n if (isProxyAlive()) {\n const endpoint = readProxyEndpoint();\n if (endpoint) return endpoint;\n }\n\n // Spawn a new proxy\n try {\n return await spawnProxy(chromeEndpoint);\n } catch (err) {\n console.warn(\n `[cdp] proxy failed, falling back to direct connection: ${err}`,\n );\n return chromeEndpoint;\n }\n}\n\n/**\n * Tools manager for Web CDP-mode MCP.\n * Connects to an existing Chrome browser via CDP (Chrome DevTools Protocol) endpoint.\n * Unlike WebPuppeteerMidsceneTools which launches its own Chrome, this connects\n * to a browser that is already running with remote debugging enabled.\n *\n * Uses a persistent WebSocket proxy to avoid repeated Chrome permission popups\n * when Chrome's settings-based remote debugging is used.\n */\nexport class WebCdpMidsceneTools extends BaseMidsceneTools<PuppeteerAgent> {\n private cdpEndpoint: string;\n private activeBrowser: Browser | null = null;\n\n constructor(cdpEndpoint: string) {\n super();\n this.cdpEndpoint = cdpEndpoint;\n }\n\n protected createTemporaryDevice() {\n return new StaticPage({\n screenshot: ScreenshotItem.create('', Date.now()),\n shotSize: { width: 1920, height: 1080 },\n shrunkShotToLogicalRatio: 1,\n });\n }\n\n protected async ensureAgent(navigateToUrl?: string): Promise<PuppeteerAgent> {\n // Re-init if URL provided\n if (this.agent && navigateToUrl) {\n try {\n await this.agent?.destroy?.();\n } catch (error) {\n console.debug('Failed to destroy agent during re-init:', error);\n }\n this.agent = undefined;\n }\n\n if (this.agent) return this.agent;\n\n // Connect via proxy to avoid repeated Chrome permission popups\n if (!this.activeBrowser) {\n const endpoint = await getProxyEndpoint(this.cdpEndpoint);\n this.activeBrowser = await puppeteer.connect({\n browserWSEndpoint: endpoint,\n defaultViewport: null,\n });\n }\n\n const browser = this.activeBrowser;\n const pages = await browser.pages();\n const webPages = pages.filter((p) => /^https?:\\/\\//.test(p.url()));\n let page: Page;\n\n if (navigateToUrl) {\n if (webPages.length > 0) {\n // Reuse an existing page and navigate it — avoids creating invisible\n // tabs when Chrome uses settings-based remote debugging (no HTTP\n // discovery endpoints, /devtools/page/* returns 403).\n page = webPages[webPages.length - 1];\n await page.bringToFront();\n await page.goto(navigateToUrl, {\n timeout: 30000,\n waitUntil: 'domcontentloaded',\n });\n } else {\n // No existing web pages — fall back to creating a new tab\n page = await browser.newPage();\n await page.goto(navigateToUrl, {\n timeout: 30000,\n waitUntil: 'domcontentloaded',\n });\n }\n } else {\n // Reuse the last web page\n page =\n webPages.length > 0\n ? webPages[webPages.length - 1]\n : pages[pages.length - 1] || (await browser.newPage());\n\n await page.bringToFront();\n }\n\n this.agent = new PuppeteerAgent(page as unknown as PuppeteerPage);\n return this.agent;\n }\n\n public async destroy(): Promise<void> {\n await super.destroy();\n if (this.activeBrowser) {\n this.activeBrowser.disconnect();\n this.activeBrowser = null;\n }\n }\n\n protected preparePlatformTools(): ToolDefinition[] {\n return [\n {\n name: 'web_connect',\n description:\n 'Connect to a web page via CDP. Opens a new tab with the given URL, or reuses the current page.',\n schema: {\n url: z\n .string()\n .url()\n .optional()\n .describe('URL to open in new tab (omit to use current page)'),\n },\n handler: async (args) => {\n const { url } = args as { url?: string };\n\n // Destroy existing agent\n if (this.agent) {\n try {\n await this.agent.destroy?.();\n } catch (e) {\n console.debug('Failed to destroy agent during connect:', e);\n }\n this.agent = undefined;\n }\n\n this.agent = await this.ensureAgent(url);\n\n const screenshot = await this.agent.page?.screenshotBase64();\n const label = url ?? 'current page';\n\n return {\n content: [\n { type: 'text', text: `Connected via CDP to: ${label}` },\n ...(screenshot ? this.buildScreenshotContent(screenshot) : []),\n ],\n };\n },\n },\n {\n name: 'web_disconnect',\n description:\n 'Disconnect from current web page. The browser stays running (managed externally).',\n schema: {},\n handler: async () => {\n if (this.agent) {\n try {\n await this.agent.destroy?.();\n } catch (e) {\n console.debug('Failed to destroy agent during disconnect:', e);\n }\n this.agent = undefined;\n }\n if (this.activeBrowser) {\n this.activeBrowser.disconnect();\n this.activeBrowser = null;\n }\n return this.buildTextResult(\n 'Disconnected from web page (browser still running externally)',\n );\n },\n },\n ];\n }\n}\n"],"names":["__webpack_require__","module","getter","definition","key","Object","obj","prop","Symbol","isProxyAlive","existsSync","PROXY_PID_FILE","pid","Number","readFileSync","process","readProxyEndpoint","PROXY_ENDPOINT_FILE","spawnProxy","chromeEndpoint","Promise","resolve","reject","proxyScript","join","__dirname","proc","spawn","output","settled","timer","setTimeout","Error","onData","chunk","lines","line","parsed","JSON","clearTimeout","err","code","getProxyEndpoint","endpoint","console","WebCdpMidsceneTools","BaseMidsceneTools","StaticPage","ScreenshotItem","Date","navigateToUrl","error","undefined","puppeteer","browser","pages","webPages","p","page","PuppeteerAgent","z","args","url","e","screenshot","label","cdpEndpoint"],"mappings":"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"}
|
|
@@ -125,7 +125,7 @@ var __webpack_exports__ = {};
|
|
|
125
125
|
const tpl = (0, utils_namespaceObject.getReportTpl)();
|
|
126
126
|
if (!tpl) throw new Error('Report template not found. Ensure @midscene/core is built correctly.');
|
|
127
127
|
let dumpScript = `<script type="midscene_web_dump">\n${(0, shared_utils_namespaceObject.escapeScriptTag)(testData.dumpString)}\n</script>`;
|
|
128
|
-
if (
|
|
128
|
+
if (testData.attributes) {
|
|
129
129
|
const attributesArr = Object.keys(testData.attributes).map((key)=>`${key}="${encodeURIComponent(testData.attributes[key])}"`);
|
|
130
130
|
dumpScript = dumpScript.replace('<script type="midscene_web_dump"', `<script type="midscene_web_dump" ${attributesArr.join(' ')}`);
|
|
131
131
|
}
|
|
@@ -177,6 +177,7 @@ var __webpack_exports__ = {};
|
|
|
177
177
|
const testData = {
|
|
178
178
|
dumpString,
|
|
179
179
|
attributes: {
|
|
180
|
+
'data-group-id': testId,
|
|
180
181
|
playwright_test_id: testId,
|
|
181
182
|
playwright_test_title: `${test.title}${projectSuffix}${retry}`,
|
|
182
183
|
playwright_test_status: result.status,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"playwright/reporter/index.js","sources":["webpack/runtime/define_property_getters","webpack/runtime/has_own_property","webpack/runtime/make_namespace_object","../../../../src/playwright/reporter/index.ts"],"sourcesContent":["__webpack_require__.d = (exports, definition) => {\n\tfor(var key in definition) {\n if(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {\n Object.defineProperty(exports, key, { enumerable: true, get: definition[key] });\n }\n }\n};","__webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))","// define __esModule on exports\n__webpack_require__.r = (exports) => {\n\tif(typeof Symbol !== 'undefined' && Symbol.toStringTag) {\n\t\tObject.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });\n\t}\n\tObject.defineProperty(exports, '__esModule', { value: true });\n};","import {\n copyFileSync,\n existsSync,\n mkdirSync,\n rmSync,\n writeFileSync,\n} from 'node:fs';\nimport { dirname, join } from 'node:path';\nimport {\n GroupedActionDump,\n type ReportDumpWithAttributes,\n} from '@midscene/core';\nimport { getReportFileName, printReportMsg } from '@midscene/core/agent';\nimport { getReportTpl } from '@midscene/core/utils';\nimport { getMidsceneRunSubDir } from '@midscene/shared/common';\nimport {\n escapeScriptTag,\n replaceIllegalPathCharsAndSpace,\n} from '@midscene/shared/utils';\nimport type {\n FullConfig,\n Reporter,\n Suite,\n TestCase,\n TestResult,\n} from '@playwright/test/reporter';\n\ninterface MidsceneReporterOptions {\n type?: 'merged' | 'separate';\n /**\n * Output format for the report.\n * - 'single-html': All screenshots embedded as base64 in a single HTML file (default)\n * - 'html-and-external-assets': Screenshots saved as separate PNG files in a screenshots/ subdirectory\n *\n * Note: 'html-and-external-assets' reports must be served via HTTP server due to CORS restrictions.\n */\n outputFormat?: 'single-html' | 'html-and-external-assets';\n}\n\nclass MidsceneReporter implements Reporter {\n private mergedFilename?: string;\n private testTitleToFilename = new Map<string, string>();\n mode?: 'merged' | 'separate';\n outputFormat: 'single-html' | 'html-and-external-assets';\n\n // Track all temp files created during this test run for cleanup\n private tempFiles = new Set<string>();\n\n // Track pending report updates\n private pendingReports = new Set<Promise<void>>();\n\n // Track whether the merged report file has been initialized\n private mergedReportInitialized = false;\n\n // Write queue to serialize file writes and prevent concurrent write conflicts\n private writeQueue: Promise<void> = Promise.resolve();\n\n // Track whether we have multiple projects (browsers)\n private hasMultipleProjects = false;\n\n // Track written screenshots to avoid duplicates (for directory mode)\n private writtenScreenshots = new Set<string>();\n\n constructor(options: MidsceneReporterOptions = {}) {\n // Set mode from constructor options (official Playwright way)\n this.mode = MidsceneReporter.getMode(options.type ?? 'merged');\n this.outputFormat = options.outputFormat ?? 'single-html';\n }\n\n private static getMode(reporterType: string): 'merged' | 'separate' {\n if (!reporterType) {\n return 'merged';\n }\n if (reporterType !== 'merged' && reporterType !== 'separate') {\n throw new Error(\n `Unknown reporter type in playwright config: ${reporterType}, only support 'merged' or 'separate'`,\n );\n }\n return reporterType;\n }\n\n private getSeparatedFilename(testTitle: string): string {\n if (!this.testTitleToFilename.has(testTitle)) {\n const baseTag = `playwright-${replaceIllegalPathCharsAndSpace(testTitle)}`;\n const generatedFilename = getReportFileName(baseTag);\n this.testTitleToFilename.set(testTitle, generatedFilename);\n }\n return this.testTitleToFilename.get(testTitle)!;\n }\n\n private getReportFilename(testTitle?: string): string {\n if (this.mode === 'merged') {\n if (!this.mergedFilename) {\n this.mergedFilename = getReportFileName('playwright-merged');\n }\n return this.mergedFilename;\n } else if (this.mode === 'separate') {\n if (!testTitle) throw new Error('testTitle is required in separate mode');\n return this.getSeparatedFilename(testTitle);\n }\n throw new Error(`Unknown mode: ${this.mode}`);\n }\n\n /**\n * Get the report path - for directory mode, returns a directory path with index.html\n */\n private getReportPath(testTitle?: string): string {\n const fileName = this.getReportFilename(testTitle);\n if (this.outputFormat === 'html-and-external-assets') {\n // Directory mode: report-name/index.html\n return join(getMidsceneRunSubDir('report'), fileName, 'index.html');\n }\n // Inline mode: report-name.html\n return join(getMidsceneRunSubDir('report'), `${fileName}.html`);\n }\n\n /**\n * Copy screenshots from temp location to report screenshots directory\n */\n private copyScreenshotsToReport(\n tempFilePath: string,\n reportPath: string,\n ): void {\n const screenshotsDir = join(dirname(reportPath), 'screenshots');\n const tempScreenshotsDir = `${tempFilePath}.screenshots`;\n\n if (!existsSync(tempScreenshotsDir)) {\n return;\n }\n\n // Ensure screenshots directory exists\n if (!existsSync(screenshotsDir)) {\n mkdirSync(screenshotsDir, { recursive: true });\n }\n\n // Read screenshot map to get all screenshot IDs\n const screenshotMapPath = `${tempFilePath}.screenshots.json`;\n if (!existsSync(screenshotMapPath)) {\n return;\n }\n\n try {\n const { readFileSync } = require('node:fs');\n const screenshotMap: Record<string, string> = JSON.parse(\n readFileSync(screenshotMapPath, 'utf-8'),\n );\n\n for (const [id, srcPath] of Object.entries(screenshotMap)) {\n // In merged mode, skip if already written to avoid duplicates\n // In separate mode, each test has its own screenshots directory\n if (this.mode === 'merged' && this.writtenScreenshots.has(id)) {\n continue;\n }\n\n const destPath = join(screenshotsDir, `${id}.png`);\n\n if (existsSync(srcPath)) {\n copyFileSync(srcPath, destPath);\n if (this.mode === 'merged') {\n this.writtenScreenshots.add(id);\n }\n }\n }\n } catch (error) {\n console.error('Error copying screenshots:', error);\n }\n }\n\n private async updateReport(testData: ReportDumpWithAttributes) {\n if (!testData || !this.mode) return;\n\n // Queue the write operation to prevent concurrent writes to the same file\n this.writeQueue = this.writeQueue.then(async () => {\n const reportPath = this.getReportPath(\n testData.attributes?.playwright_test_title,\n );\n\n // Ensure report directory exists for directory mode\n if (this.outputFormat === 'html-and-external-assets') {\n const reportDir = dirname(reportPath);\n if (!existsSync(reportDir)) {\n mkdirSync(reportDir, { recursive: true });\n }\n }\n\n // Get report template\n const tpl = getReportTpl();\n if (!tpl) {\n throw new Error(\n 'Report template not found. Ensure @midscene/core is built correctly.',\n );\n }\n\n // Parse the dump string and generate dump script tag\n let dumpScript = `<script type=\"midscene_web_dump\">\\n${escapeScriptTag(testData.dumpString)}\\n</script>`;\n\n // Add attributes to the dump script if this is merged report\n if (this.mode === 'merged' && testData.attributes) {\n const attributesArr = Object.keys(testData.attributes).map((key) => {\n return `${key}=\"${encodeURIComponent(testData.attributes![key])}\"`;\n });\n // Add attributes to the script tag\n dumpScript = dumpScript.replace(\n '<script type=\"midscene_web_dump\"',\n `<script type=\"midscene_web_dump\" ${attributesArr.join(' ')}`,\n );\n }\n\n // Write or append to file\n if (this.mode === 'merged') {\n // For merged report, write template + dump on first write, then only append dumps\n if (!this.mergedReportInitialized) {\n writeFileSync(reportPath, tpl + dumpScript, { flag: 'w' });\n this.mergedReportInitialized = true;\n } else {\n // Append only the dump scripts for subsequent tests\n writeFileSync(reportPath, dumpScript, { flag: 'a' });\n }\n } else {\n // For separate reports, write each test to its own file with template\n writeFileSync(reportPath, tpl + dumpScript, { flag: 'w' });\n }\n\n printReportMsg(reportPath);\n });\n\n await this.writeQueue;\n }\n\n async onBegin(config: FullConfig, suite: Suite) {\n // Check if we have multiple projects to determine if we need browser labels\n this.hasMultipleProjects = (config.projects?.length || 0) > 1;\n }\n\n onTestBegin(_test: TestCase, _result: TestResult) {\n // logger(`Starting test ${test.title}`);\n }\n\n onTestEnd(test: TestCase, result: TestResult) {\n const dumpAnnotation = test.annotations.find((annotation) => {\n return annotation.type === 'MIDSCENE_DUMP_ANNOTATION';\n });\n if (!dumpAnnotation?.description) return;\n\n const tempFilePath = dumpAnnotation.description;\n\n // Track temp files for potential cleanup in onEnd\n for (const filePath of GroupedActionDump.getFilePaths(tempFilePath)) {\n this.tempFiles.add(filePath);\n }\n\n let dumpString: string | undefined;\n\n try {\n if (this.outputFormat === 'html-and-external-assets') {\n // Directory mode: keep { $screenshot: id } format, copy screenshots to report dir\n const { readFileSync } = require('node:fs');\n dumpString = readFileSync(tempFilePath, 'utf-8');\n\n // Get report path and copy screenshots\n const retry = result.retry ? `(retry #${result.retry})` : '';\n const projectName = this.hasMultipleProjects\n ? test.parent?.project()?.name\n : undefined;\n const projectSuffix = projectName ? ` [${projectName}]` : '';\n const testTitle = `${test.title}${projectSuffix}${retry}`;\n const reportPath = this.getReportPath(testTitle);\n\n this.copyScreenshotsToReport(tempFilePath, reportPath);\n } else {\n // Inline mode: convert screenshots to base64\n dumpString = GroupedActionDump.fromFilesAsInlineJson(tempFilePath);\n }\n } catch (error) {\n console.error(\n `Failed to read Midscene dump file: ${tempFilePath}`,\n error,\n );\n // Don't return here - we still need to clean up the temp file\n }\n\n // Only update report if we successfully read the dump\n if (dumpString) {\n const retry = result.retry ? `(retry #${result.retry})` : '';\n const testId = `${test.id}${retry}`;\n\n // Get the project name (browser name) only if we have multiple projects\n const projectName = this.hasMultipleProjects\n ? test.parent?.project()?.name\n : undefined;\n const projectSuffix = projectName ? ` [${projectName}]` : '';\n\n const testData: ReportDumpWithAttributes = {\n dumpString,\n attributes: {\n playwright_test_id: testId,\n playwright_test_title: `${test.title}${projectSuffix}${retry}`,\n playwright_test_status: result.status,\n playwright_test_duration: result.duration,\n },\n };\n\n // Start async report update and track it\n const reportPromise = this.updateReport(testData)\n .catch((error) => {\n console.error('Error updating report:', error);\n })\n .finally(() => {\n this.pendingReports.delete(reportPromise);\n });\n this.pendingReports.add(reportPromise);\n }\n\n // Always try to clean up temp files\n try {\n GroupedActionDump.cleanupFiles(tempFilePath);\n for (const filePath of GroupedActionDump.getFilePaths(tempFilePath)) {\n this.tempFiles.delete(filePath);\n }\n } catch {\n // Keep in tempFiles for cleanup in onEnd\n }\n }\n\n async onEnd() {\n // Wait for all pending report updates to complete\n if (this.pendingReports.size > 0) {\n console.log(\n `Midscene: Waiting for ${this.pendingReports.size} pending report(s) to complete...`,\n );\n await Promise.all(Array.from(this.pendingReports));\n }\n\n // Print directory mode notice (only for merged mode)\n if (\n this.outputFormat === 'html-and-external-assets' &&\n this.mode === 'merged'\n ) {\n const reportPath = this.getReportPath();\n const reportDir = dirname(reportPath);\n console.log('[Midscene] Directory report generated.');\n console.log(\n '[Midscene] Note: This report must be served via HTTP server due to CORS restrictions.',\n );\n console.log(`[Midscene] Example: npx serve ${reportDir}`);\n } else if (\n this.outputFormat === 'html-and-external-assets' &&\n this.mode === 'separate'\n ) {\n const reportBaseDir = getMidsceneRunSubDir('report');\n console.log('[Midscene] Directory reports generated.');\n console.log(\n '[Midscene] Note: Reports must be served via HTTP server due to CORS restrictions.',\n );\n console.log(`[Midscene] Example: npx serve ${reportBaseDir}`);\n }\n\n // Clean up any remaining temp files that weren't deleted in onTestEnd\n if (this.tempFiles.size > 0) {\n console.log(\n `Midscene: Cleaning up ${this.tempFiles.size} remaining temp file(s)...`,\n );\n\n for (const filePath of this.tempFiles) {\n try {\n rmSync(filePath, { force: true });\n } catch (error) {\n // Silently ignore - file may have been deleted already\n }\n }\n\n this.tempFiles.clear();\n }\n }\n}\n\nexport default MidsceneReporter;\n"],"names":["__webpack_require__","definition","key","Object","obj","prop","Symbol","MidsceneReporter","reporterType","Error","testTitle","baseTag","replaceIllegalPathCharsAndSpace","generatedFilename","getReportFileName","fileName","join","getMidsceneRunSubDir","tempFilePath","reportPath","screenshotsDir","dirname","tempScreenshotsDir","existsSync","mkdirSync","screenshotMapPath","readFileSync","require","screenshotMap","JSON","id","srcPath","destPath","copyFileSync","error","console","testData","reportDir","tpl","getReportTpl","dumpScript","escapeScriptTag","attributesArr","encodeURIComponent","writeFileSync","printReportMsg","config","suite","_test","_result","test","result","dumpAnnotation","annotation","filePath","GroupedActionDump","dumpString","retry","projectName","undefined","projectSuffix","testId","reportPromise","Promise","Array","reportBaseDir","rmSync","options","Map","Set"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"playwright/reporter/index.js","sources":["webpack/runtime/define_property_getters","webpack/runtime/has_own_property","webpack/runtime/make_namespace_object","../../../../src/playwright/reporter/index.ts"],"sourcesContent":["__webpack_require__.d = (exports, definition) => {\n\tfor(var key in definition) {\n if(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {\n Object.defineProperty(exports, key, { enumerable: true, get: definition[key] });\n }\n }\n};","__webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))","// define __esModule on exports\n__webpack_require__.r = (exports) => {\n\tif(typeof Symbol !== 'undefined' && Symbol.toStringTag) {\n\t\tObject.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });\n\t}\n\tObject.defineProperty(exports, '__esModule', { value: true });\n};","import {\n copyFileSync,\n existsSync,\n mkdirSync,\n rmSync,\n writeFileSync,\n} from 'node:fs';\nimport { dirname, join } from 'node:path';\nimport {\n GroupedActionDump,\n type ReportDumpWithAttributes,\n} from '@midscene/core';\nimport { getReportFileName, printReportMsg } from '@midscene/core/agent';\nimport { getReportTpl } from '@midscene/core/utils';\nimport { getMidsceneRunSubDir } from '@midscene/shared/common';\nimport {\n escapeScriptTag,\n replaceIllegalPathCharsAndSpace,\n} from '@midscene/shared/utils';\nimport type {\n FullConfig,\n Reporter,\n Suite,\n TestCase,\n TestResult,\n} from '@playwright/test/reporter';\n\ninterface MidsceneReporterOptions {\n type?: 'merged' | 'separate';\n /**\n * Output format for the report.\n * - 'single-html': All screenshots embedded as base64 in a single HTML file (default)\n * - 'html-and-external-assets': Screenshots saved as separate PNG files in a screenshots/ subdirectory\n *\n * Note: 'html-and-external-assets' reports must be served via HTTP server due to CORS restrictions.\n */\n outputFormat?: 'single-html' | 'html-and-external-assets';\n}\n\nclass MidsceneReporter implements Reporter {\n private mergedFilename?: string;\n private testTitleToFilename = new Map<string, string>();\n mode?: 'merged' | 'separate';\n outputFormat: 'single-html' | 'html-and-external-assets';\n\n // Track all temp files created during this test run for cleanup\n private tempFiles = new Set<string>();\n\n // Track pending report updates\n private pendingReports = new Set<Promise<void>>();\n\n // Track whether the merged report file has been initialized\n private mergedReportInitialized = false;\n\n // Write queue to serialize file writes and prevent concurrent write conflicts\n private writeQueue: Promise<void> = Promise.resolve();\n\n // Track whether we have multiple projects (browsers)\n private hasMultipleProjects = false;\n\n // Track written screenshots to avoid duplicates (for directory mode)\n private writtenScreenshots = new Set<string>();\n\n constructor(options: MidsceneReporterOptions = {}) {\n // Set mode from constructor options (official Playwright way)\n this.mode = MidsceneReporter.getMode(options.type ?? 'merged');\n this.outputFormat = options.outputFormat ?? 'single-html';\n }\n\n private static getMode(reporterType: string): 'merged' | 'separate' {\n if (!reporterType) {\n return 'merged';\n }\n if (reporterType !== 'merged' && reporterType !== 'separate') {\n throw new Error(\n `Unknown reporter type in playwright config: ${reporterType}, only support 'merged' or 'separate'`,\n );\n }\n return reporterType;\n }\n\n private getSeparatedFilename(testTitle: string): string {\n if (!this.testTitleToFilename.has(testTitle)) {\n const baseTag = `playwright-${replaceIllegalPathCharsAndSpace(testTitle)}`;\n const generatedFilename = getReportFileName(baseTag);\n this.testTitleToFilename.set(testTitle, generatedFilename);\n }\n return this.testTitleToFilename.get(testTitle)!;\n }\n\n private getReportFilename(testTitle?: string): string {\n if (this.mode === 'merged') {\n if (!this.mergedFilename) {\n this.mergedFilename = getReportFileName('playwright-merged');\n }\n return this.mergedFilename;\n } else if (this.mode === 'separate') {\n if (!testTitle) throw new Error('testTitle is required in separate mode');\n return this.getSeparatedFilename(testTitle);\n }\n throw new Error(`Unknown mode: ${this.mode}`);\n }\n\n /**\n * Get the report path - for directory mode, returns a directory path with index.html\n */\n private getReportPath(testTitle?: string): string {\n const fileName = this.getReportFilename(testTitle);\n if (this.outputFormat === 'html-and-external-assets') {\n // Directory mode: report-name/index.html\n return join(getMidsceneRunSubDir('report'), fileName, 'index.html');\n }\n // Inline mode: report-name.html\n return join(getMidsceneRunSubDir('report'), `${fileName}.html`);\n }\n\n /**\n * Copy screenshots from temp location to report screenshots directory\n */\n private copyScreenshotsToReport(\n tempFilePath: string,\n reportPath: string,\n ): void {\n const screenshotsDir = join(dirname(reportPath), 'screenshots');\n const tempScreenshotsDir = `${tempFilePath}.screenshots`;\n\n if (!existsSync(tempScreenshotsDir)) {\n return;\n }\n\n // Ensure screenshots directory exists\n if (!existsSync(screenshotsDir)) {\n mkdirSync(screenshotsDir, { recursive: true });\n }\n\n // Read screenshot map to get all screenshot IDs\n const screenshotMapPath = `${tempFilePath}.screenshots.json`;\n if (!existsSync(screenshotMapPath)) {\n return;\n }\n\n try {\n const { readFileSync } = require('node:fs');\n const screenshotMap: Record<string, string> = JSON.parse(\n readFileSync(screenshotMapPath, 'utf-8'),\n );\n\n for (const [id, srcPath] of Object.entries(screenshotMap)) {\n // In merged mode, skip if already written to avoid duplicates\n // In separate mode, each test has its own screenshots directory\n if (this.mode === 'merged' && this.writtenScreenshots.has(id)) {\n continue;\n }\n\n const destPath = join(screenshotsDir, `${id}.png`);\n\n if (existsSync(srcPath)) {\n copyFileSync(srcPath, destPath);\n if (this.mode === 'merged') {\n this.writtenScreenshots.add(id);\n }\n }\n }\n } catch (error) {\n console.error('Error copying screenshots:', error);\n }\n }\n\n private async updateReport(testData: ReportDumpWithAttributes) {\n if (!testData || !this.mode) return;\n\n // Queue the write operation to prevent concurrent writes to the same file\n this.writeQueue = this.writeQueue.then(async () => {\n const reportPath = this.getReportPath(\n testData.attributes?.playwright_test_title,\n );\n\n // Ensure report directory exists for directory mode\n if (this.outputFormat === 'html-and-external-assets') {\n const reportDir = dirname(reportPath);\n if (!existsSync(reportDir)) {\n mkdirSync(reportDir, { recursive: true });\n }\n }\n\n // Get report template\n const tpl = getReportTpl();\n if (!tpl) {\n throw new Error(\n 'Report template not found. Ensure @midscene/core is built correctly.',\n );\n }\n\n // Parse the dump string and generate dump script tag\n let dumpScript = `<script type=\"midscene_web_dump\">\\n${escapeScriptTag(testData.dumpString)}\\n</script>`;\n\n if (testData.attributes) {\n const attributesArr = Object.keys(testData.attributes).map((key) => {\n return `${key}=\"${encodeURIComponent(testData.attributes![key])}\"`;\n });\n // Add attributes to the script tag\n dumpScript = dumpScript.replace(\n '<script type=\"midscene_web_dump\"',\n `<script type=\"midscene_web_dump\" ${attributesArr.join(' ')}`,\n );\n }\n\n // Write or append to file\n if (this.mode === 'merged') {\n // For merged report, write template + dump on first write, then only append dumps\n if (!this.mergedReportInitialized) {\n writeFileSync(reportPath, tpl + dumpScript, { flag: 'w' });\n this.mergedReportInitialized = true;\n } else {\n // Append only the dump scripts for subsequent tests\n writeFileSync(reportPath, dumpScript, { flag: 'a' });\n }\n } else {\n // For separate reports, write each test to its own file with template\n writeFileSync(reportPath, tpl + dumpScript, { flag: 'w' });\n }\n\n printReportMsg(reportPath);\n });\n\n await this.writeQueue;\n }\n\n async onBegin(config: FullConfig, suite: Suite) {\n // Check if we have multiple projects to determine if we need browser labels\n this.hasMultipleProjects = (config.projects?.length || 0) > 1;\n }\n\n onTestBegin(_test: TestCase, _result: TestResult) {\n // logger(`Starting test ${test.title}`);\n }\n\n onTestEnd(test: TestCase, result: TestResult) {\n const dumpAnnotation = test.annotations.find((annotation) => {\n return annotation.type === 'MIDSCENE_DUMP_ANNOTATION';\n });\n if (!dumpAnnotation?.description) return;\n\n const tempFilePath = dumpAnnotation.description;\n\n // Track temp files for potential cleanup in onEnd\n for (const filePath of GroupedActionDump.getFilePaths(tempFilePath)) {\n this.tempFiles.add(filePath);\n }\n\n let dumpString: string | undefined;\n\n try {\n if (this.outputFormat === 'html-and-external-assets') {\n // Directory mode: keep { $screenshot: id } format, copy screenshots to report dir\n const { readFileSync } = require('node:fs');\n dumpString = readFileSync(tempFilePath, 'utf-8');\n\n // Get report path and copy screenshots\n const retry = result.retry ? `(retry #${result.retry})` : '';\n const projectName = this.hasMultipleProjects\n ? test.parent?.project()?.name\n : undefined;\n const projectSuffix = projectName ? ` [${projectName}]` : '';\n const testTitle = `${test.title}${projectSuffix}${retry}`;\n const reportPath = this.getReportPath(testTitle);\n\n this.copyScreenshotsToReport(tempFilePath, reportPath);\n } else {\n // Inline mode: convert screenshots to base64\n dumpString = GroupedActionDump.fromFilesAsInlineJson(tempFilePath);\n }\n } catch (error) {\n console.error(\n `Failed to read Midscene dump file: ${tempFilePath}`,\n error,\n );\n // Don't return here - we still need to clean up the temp file\n }\n\n // Only update report if we successfully read the dump\n if (dumpString) {\n const retry = result.retry ? `(retry #${result.retry})` : '';\n const testId = `${test.id}${retry}`;\n\n // Get the project name (browser name) only if we have multiple projects\n const projectName = this.hasMultipleProjects\n ? test.parent?.project()?.name\n : undefined;\n const projectSuffix = projectName ? ` [${projectName}]` : '';\n\n const testData: ReportDumpWithAttributes = {\n dumpString,\n attributes: {\n 'data-group-id': testId,\n playwright_test_id: testId,\n playwright_test_title: `${test.title}${projectSuffix}${retry}`,\n playwright_test_status: result.status,\n playwright_test_duration: result.duration,\n },\n };\n\n // Start async report update and track it\n const reportPromise = this.updateReport(testData)\n .catch((error) => {\n console.error('Error updating report:', error);\n })\n .finally(() => {\n this.pendingReports.delete(reportPromise);\n });\n this.pendingReports.add(reportPromise);\n }\n\n // Always try to clean up temp files\n try {\n GroupedActionDump.cleanupFiles(tempFilePath);\n for (const filePath of GroupedActionDump.getFilePaths(tempFilePath)) {\n this.tempFiles.delete(filePath);\n }\n } catch {\n // Keep in tempFiles for cleanup in onEnd\n }\n }\n\n async onEnd() {\n // Wait for all pending report updates to complete\n if (this.pendingReports.size > 0) {\n console.log(\n `Midscene: Waiting for ${this.pendingReports.size} pending report(s) to complete...`,\n );\n await Promise.all(Array.from(this.pendingReports));\n }\n\n // Print directory mode notice (only for merged mode)\n if (\n this.outputFormat === 'html-and-external-assets' &&\n this.mode === 'merged'\n ) {\n const reportPath = this.getReportPath();\n const reportDir = dirname(reportPath);\n console.log('[Midscene] Directory report generated.');\n console.log(\n '[Midscene] Note: This report must be served via HTTP server due to CORS restrictions.',\n );\n console.log(`[Midscene] Example: npx serve ${reportDir}`);\n } else if (\n this.outputFormat === 'html-and-external-assets' &&\n this.mode === 'separate'\n ) {\n const reportBaseDir = getMidsceneRunSubDir('report');\n console.log('[Midscene] Directory reports generated.');\n console.log(\n '[Midscene] Note: Reports must be served via HTTP server due to CORS restrictions.',\n );\n console.log(`[Midscene] Example: npx serve ${reportBaseDir}`);\n }\n\n // Clean up any remaining temp files that weren't deleted in onTestEnd\n if (this.tempFiles.size > 0) {\n console.log(\n `Midscene: Cleaning up ${this.tempFiles.size} remaining temp file(s)...`,\n );\n\n for (const filePath of this.tempFiles) {\n try {\n rmSync(filePath, { force: true });\n } catch (error) {\n // Silently ignore - file may have been deleted already\n }\n }\n\n this.tempFiles.clear();\n }\n }\n}\n\nexport default MidsceneReporter;\n"],"names":["__webpack_require__","definition","key","Object","obj","prop","Symbol","MidsceneReporter","reporterType","Error","testTitle","baseTag","replaceIllegalPathCharsAndSpace","generatedFilename","getReportFileName","fileName","join","getMidsceneRunSubDir","tempFilePath","reportPath","screenshotsDir","dirname","tempScreenshotsDir","existsSync","mkdirSync","screenshotMapPath","readFileSync","require","screenshotMap","JSON","id","srcPath","destPath","copyFileSync","error","console","testData","reportDir","tpl","getReportTpl","dumpScript","escapeScriptTag","attributesArr","encodeURIComponent","writeFileSync","printReportMsg","config","suite","_test","_result","test","result","dumpAnnotation","annotation","filePath","GroupedActionDump","dumpString","retry","projectName","undefined","projectSuffix","testId","reportPromise","Promise","Array","reportBaseDir","rmSync","options","Map","Set"],"mappings":"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"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CDP WebSocket Proxy — standalone process.
|
|
3
|
+
*
|
|
4
|
+
* Holds a single persistent WebSocket connection to Chrome's CDP endpoint and
|
|
5
|
+
* exposes a local WebSocket server. Midscene CLI processes connect to the proxy
|
|
6
|
+
* instead of Chrome directly, so Chrome's "Allow remote debugging" permission
|
|
7
|
+
* popup only fires once (when the proxy connects).
|
|
8
|
+
*
|
|
9
|
+
* Exit conditions:
|
|
10
|
+
* 1. Upstream Chrome connection closes or errors.
|
|
11
|
+
* 2. No downstream client message for IDLE_TIMEOUT_MS (default 5 min).
|
|
12
|
+
* 3. SIGTERM / SIGINT.
|
|
13
|
+
*
|
|
14
|
+
* Usage (spawned by mcp-tools-cdp.ts):
|
|
15
|
+
* node cdp-proxy.js <chrome-ws-endpoint>
|
|
16
|
+
*
|
|
17
|
+
* On startup, prints the proxy endpoint to stdout as a single JSON line:
|
|
18
|
+
* {"endpoint":"ws://127.0.0.1:<port>/devtools/browser"}
|
|
19
|
+
* and writes the same endpoint to PROXY_ENDPOINT_FILE.
|
|
20
|
+
*/
|
|
21
|
+
export {};
|
|
@@ -6,6 +6,9 @@ import { StaticPage } from './static';
|
|
|
6
6
|
* Connects to an existing Chrome browser via CDP (Chrome DevTools Protocol) endpoint.
|
|
7
7
|
* Unlike WebPuppeteerMidsceneTools which launches its own Chrome, this connects
|
|
8
8
|
* to a browser that is already running with remote debugging enabled.
|
|
9
|
+
*
|
|
10
|
+
* Uses a persistent WebSocket proxy to avoid repeated Chrome permission popups
|
|
11
|
+
* when Chrome's settings-based remote debugging is used.
|
|
9
12
|
*/
|
|
10
13
|
export declare class WebCdpMidsceneTools extends BaseMidsceneTools<PuppeteerAgent> {
|
|
11
14
|
private cdpEndpoint;
|
package/package.json
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
"Browser use",
|
|
9
9
|
"Android use"
|
|
10
10
|
],
|
|
11
|
-
"version": "1.
|
|
11
|
+
"version": "1.6.0",
|
|
12
12
|
"repository": "https://github.com/web-infra-dev/midscene",
|
|
13
13
|
"homepage": "https://midscenejs.com/",
|
|
14
14
|
"main": "./dist/lib/index.js",
|
|
@@ -109,9 +109,10 @@
|
|
|
109
109
|
"puppeteer-core": "24.6.0",
|
|
110
110
|
"socket.io": "^4.8.1",
|
|
111
111
|
"socket.io-client": "4.8.1",
|
|
112
|
-
"
|
|
113
|
-
"@midscene/
|
|
114
|
-
"@midscene/
|
|
112
|
+
"ws": "^8.18.1",
|
|
113
|
+
"@midscene/core": "1.6.0",
|
|
114
|
+
"@midscene/playground": "1.6.0",
|
|
115
|
+
"@midscene/shared": "1.6.0"
|
|
115
116
|
},
|
|
116
117
|
"devDependencies": {
|
|
117
118
|
"@playwright/test": "^1.45.0",
|
|
@@ -122,6 +123,7 @@
|
|
|
122
123
|
"@types/js-yaml": "4.0.9",
|
|
123
124
|
"@types/node": "^18.0.0",
|
|
124
125
|
"@types/semver": "7.7.0",
|
|
126
|
+
"@types/ws": "8.18.1",
|
|
125
127
|
"devtools-protocol": "0.0.1380148",
|
|
126
128
|
"js-yaml": "4.1.0",
|
|
127
129
|
"playwright": "^1.45.0",
|