stably 4.12.4 → 4.12.5
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/index.mjs +1 -1
- package/dist/node_modules/playwright/lib/cli/daemon/daemon.js +0 -43
- package/dist/node_modules/playwright/lib/index.js +1 -32
- package/dist/node_modules/playwright/lib/mcp/browser/browserServerBackend.js +0 -27
- package/dist/node_modules/playwright/lib/mcp/browser/context.js +0 -52
- package/dist/node_modules/playwright/lib/mcp/browser/sessionLog.js +0 -50
- package/dist/node_modules/playwright/lib/mcp/test/browserBackend.js +1 -6
- package/dist/stably-browser.js +56 -31
- package/package.json +2 -2
- package/dist/node_modules/playwright/lib/mcp/browser/config.js.rej +0 -11
|
@@ -75,49 +75,6 @@ async function startMcpDaemonServer(config, contextFactory) {
|
|
|
75
75
|
timestamp: Date.now()
|
|
76
76
|
};
|
|
77
77
|
const { browserContext, close } = await contextFactory.createContext(clientInfo, new AbortController().signal, {});
|
|
78
|
-
// For CDP connections: resize the browser window to show chrome (tabs, URL bar).
|
|
79
|
-
// Without this, the remote browser's viewport fills the entire window, hiding chrome.
|
|
80
|
-
if (config.browser.cdpEndpoint) {
|
|
81
|
-
try {
|
|
82
|
-
const pages = browserContext.pages();
|
|
83
|
-
const page = pages[0];
|
|
84
|
-
if (page) {
|
|
85
|
-
// Try to load viewport from the user's playwright.config (most authoritative source),
|
|
86
|
-
// then fall back to config.browser.contextOptions.viewport (which may just be the 1280x720 default).
|
|
87
|
-
let viewport = null;
|
|
88
|
-
if (config.playwrightConfigPath) {
|
|
89
|
-
try {
|
|
90
|
-
const { requireOrImport } = require("../../transform/transform");
|
|
91
|
-
let userConfig = await requireOrImport(config.playwrightConfigPath);
|
|
92
|
-
if (userConfig && typeof userConfig === "object" && "default" in userConfig)
|
|
93
|
-
userConfig = userConfig.default;
|
|
94
|
-
viewport = userConfig?.use?.viewport || userConfig?.projects?.[0]?.use?.viewport;
|
|
95
|
-
daemonDebug("extracted viewport from playwright config:", viewport);
|
|
96
|
-
} catch (e) {
|
|
97
|
-
daemonDebug("failed to load playwright config for viewport", e);
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
// Fall back to config viewport if playwright.config didn't provide one
|
|
101
|
-
if (!viewport) {
|
|
102
|
-
viewport = config.browser.contextOptions?.viewport;
|
|
103
|
-
}
|
|
104
|
-
if (viewport && viewport.width && viewport.height) {
|
|
105
|
-
await page.setViewportSize(viewport);
|
|
106
|
-
}
|
|
107
|
-
const cdpSession = await browserContext.newCDPSession(page);
|
|
108
|
-
const { windowId } = await cdpSession.send("Browser.getWindowForTarget");
|
|
109
|
-
const vp = viewport || { width: 1280, height: 720 };
|
|
110
|
-
const CHROME_HEADER_HEIGHT = 85;
|
|
111
|
-
await cdpSession.send("Browser.setWindowBounds", {
|
|
112
|
-
windowId,
|
|
113
|
-
bounds: { width: vp.width, height: vp.height + CHROME_HEADER_HEIGHT }
|
|
114
|
-
});
|
|
115
|
-
await cdpSession.detach();
|
|
116
|
-
}
|
|
117
|
-
} catch (e) {
|
|
118
|
-
daemonDebug("failed to resize window for CDP connection (best-effort)", e);
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
78
|
browserContext.on("close", () => {
|
|
122
79
|
daemonDebug("browser closed, shutting down daemon");
|
|
123
80
|
shutdown(0);
|
|
@@ -92,21 +92,6 @@ const playwrightFixtures = {
|
|
|
92
92
|
browser: [async ({ playwright, browserName, _browserOptions, connectOptions }, use) => {
|
|
93
93
|
if (!["chromium", "firefox", "webkit"].includes(browserName))
|
|
94
94
|
throw new Error(`Unexpected browserName "${browserName}", must be one of "chromium", "firefox" or "webkit"`);
|
|
95
|
-
// CDP endpoint support via environment variable or connectOptions with /browser-cdp/ path
|
|
96
|
-
const cdpEndpoint = process.env.PLAYWRIGHT_CDP_ENDPOINT;
|
|
97
|
-
const connectWsEndpoint = connectOptions == null ? void 0 : connectOptions.wsEndpoint;
|
|
98
|
-
const isCdpWsEndpoint = typeof connectWsEndpoint === "string" && (connectWsEndpoint.includes("/browser-cdp/") || connectWsEndpoint.includes("/browser/cdp"));
|
|
99
|
-
if (cdpEndpoint || isCdpWsEndpoint) {
|
|
100
|
-
if (browserName !== "chromium")
|
|
101
|
-
throw new Error("CDP endpoint is only supported for Chromium browsers");
|
|
102
|
-
const endpoint = cdpEndpoint || connectWsEndpoint;
|
|
103
|
-
const cdpHeadersFromEnv = process.env.PLAYWRIGHT_CDP_HEADERS ? JSON.parse(process.env.PLAYWRIGHT_CDP_HEADERS) : void 0;
|
|
104
|
-
const browser2 = await playwright.chromium.connectOverCDP(endpoint, {
|
|
105
|
-
headers: cdpHeadersFromEnv || (connectOptions == null ? void 0 : connectOptions.headers)
|
|
106
|
-
});
|
|
107
|
-
await use(browser2);
|
|
108
|
-
return;
|
|
109
|
-
}
|
|
110
95
|
if (connectOptions) {
|
|
111
96
|
const browser2 = await playwright[browserName].connect({
|
|
112
97
|
...connectOptions,
|
|
@@ -350,13 +335,7 @@ const playwrightFixtures = {
|
|
|
350
335
|
size: typeof video === "string" ? void 0 : video.size
|
|
351
336
|
}
|
|
352
337
|
} : {};
|
|
353
|
-
|
|
354
|
-
// creating a new one. This preserves browser state (pages, cookies, etc.)
|
|
355
|
-
// after the test completes, allowing the daemon to interact with the result.
|
|
356
|
-
const isCdpSession = !!process.env.PLAYWRIGHT_CDP_ENDPOINT;
|
|
357
|
-
const context = isCdpSession && browser.contexts().length > 0
|
|
358
|
-
? browser.contexts()[0]
|
|
359
|
-
: await browser.newContext({ ...videoOptions, ...options });
|
|
338
|
+
const context = await browser.newContext({ ...videoOptions, ...options });
|
|
360
339
|
if (process.env.PW_CLOCK === "frozen") {
|
|
361
340
|
await context._wrapApiCall(async () => {
|
|
362
341
|
await context.clock.install({ time: 0 });
|
|
@@ -372,10 +351,6 @@ const playwrightFixtures = {
|
|
|
372
351
|
if (closed)
|
|
373
352
|
return;
|
|
374
353
|
closed = true;
|
|
375
|
-
// Skip context.close() when connected via CDP — preserve browser state
|
|
376
|
-
// so the daemon can continue interacting with the test's pages.
|
|
377
|
-
if (isCdpSession)
|
|
378
|
-
return;
|
|
379
354
|
const closeReason = testInfo.status === "timedOut" ? "Test timeout of " + testInfo.timeout + "ms exceeded." : "Test ended.";
|
|
380
355
|
await context.close({ reason: closeReason });
|
|
381
356
|
const testFailed = testInfo.status !== testInfo.expectedStatus;
|
|
@@ -428,12 +403,6 @@ const playwrightFixtures = {
|
|
|
428
403
|
await browserImpl._wrapApiCall(() => browserImpl._disconnectFromReusedContext(closeReason), { internal: true });
|
|
429
404
|
},
|
|
430
405
|
page: async ({ context, _reuseContext }, use) => {
|
|
431
|
-
// When connected via CDP, reuse the existing page (the daemon's page)
|
|
432
|
-
// instead of creating a new one, so the daemon sees the test's navigation.
|
|
433
|
-
if (process.env.PLAYWRIGHT_CDP_ENDPOINT && context.pages().length > 0) {
|
|
434
|
-
await use(context.pages()[0]);
|
|
435
|
-
return;
|
|
436
|
-
}
|
|
437
406
|
if (!_reuseContext) {
|
|
438
407
|
await use(await context.newPage());
|
|
439
408
|
return;
|
|
@@ -27,22 +27,6 @@ var import_response = require("./response");
|
|
|
27
27
|
var import_sessionLog = require("./sessionLog");
|
|
28
28
|
var import_tools = require("./tools");
|
|
29
29
|
var import_tool = require("../sdk/tool");
|
|
30
|
-
var import_net = require("net");
|
|
31
|
-
function sendRecorderEvent(event) {
|
|
32
|
-
const port = process.env.CLAUDE_RECORDER_PORT;
|
|
33
|
-
if (!port)
|
|
34
|
-
return;
|
|
35
|
-
try {
|
|
36
|
-
const socket = import_net.createConnection({
|
|
37
|
-
port: Number(port),
|
|
38
|
-
host: process.env.CLAUDE_RECORDER_HOST || "127.0.0.1"
|
|
39
|
-
});
|
|
40
|
-
const payload = JSON.stringify(event) + "\n";
|
|
41
|
-
socket.on("error", () => socket.destroy());
|
|
42
|
-
socket.write(payload, () => socket.end());
|
|
43
|
-
} catch (error) {
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
30
|
class BrowserServerBackend {
|
|
47
31
|
constructor(config, factory, options = {}) {
|
|
48
32
|
this._config = config;
|
|
@@ -80,17 +64,6 @@ Tool "${name}" not found` }],
|
|
|
80
64
|
await tool.handle(context, parsedArguments, response);
|
|
81
65
|
responseObject = await response.serialize();
|
|
82
66
|
this._sessionLog?.logResponse(name, parsedArguments, responseObject);
|
|
83
|
-
// Send recorder event for user action tracking
|
|
84
|
-
const parsed = (0, import_response.parseResponse)(responseObject);
|
|
85
|
-
if (parsed && parsed.code) {
|
|
86
|
-
sendRecorderEvent({
|
|
87
|
-
type: "user-action",
|
|
88
|
-
action: { name: name, args: parsedArguments },
|
|
89
|
-
code: parsed.code,
|
|
90
|
-
url: this._context?.currentTab()?.page?.url() || "",
|
|
91
|
-
timestamp: Date.now()
|
|
92
|
-
});
|
|
93
|
-
}
|
|
94
67
|
} catch (error) {
|
|
95
68
|
return {
|
|
96
69
|
content: [{ type: "text", text: `### Error
|
|
@@ -40,55 +40,6 @@ var import_log = require("../log");
|
|
|
40
40
|
var import_tab = require("./tab");
|
|
41
41
|
var import_config = require("./config");
|
|
42
42
|
const testDebug = (0, import_utilsBundle.debug)("pw:mcp:test");
|
|
43
|
-
class InputRecorder {
|
|
44
|
-
constructor(context, browserContext) {
|
|
45
|
-
this._context = context;
|
|
46
|
-
this._browserContext = browserContext;
|
|
47
|
-
}
|
|
48
|
-
static async create(context, browserContext) {
|
|
49
|
-
const recorder = new InputRecorder(context, browserContext);
|
|
50
|
-
await recorder._initialize();
|
|
51
|
-
return recorder;
|
|
52
|
-
}
|
|
53
|
-
async _initialize() {
|
|
54
|
-
const sessionLog = this._context.sessionLog;
|
|
55
|
-
if (!sessionLog) return;
|
|
56
|
-
await this._browserContext._enableRecorder({
|
|
57
|
-
mode: "recording",
|
|
58
|
-
recorderMode: "api"
|
|
59
|
-
}, {
|
|
60
|
-
actionAdded: (page, data, code) => {
|
|
61
|
-
if (this._context.isRunningTool())
|
|
62
|
-
return;
|
|
63
|
-
const tab = import_tab.Tab.forPage(page);
|
|
64
|
-
if (tab)
|
|
65
|
-
sessionLog.logUserAction(data.action, tab, code, false);
|
|
66
|
-
},
|
|
67
|
-
actionUpdated: (page, data, code) => {
|
|
68
|
-
if (this._context.isRunningTool())
|
|
69
|
-
return;
|
|
70
|
-
const tab = import_tab.Tab.forPage(page);
|
|
71
|
-
if (tab)
|
|
72
|
-
sessionLog.logUserAction(data.action, tab, code, true);
|
|
73
|
-
},
|
|
74
|
-
signalAdded: (page, data) => {
|
|
75
|
-
if (this._context.isRunningTool())
|
|
76
|
-
return;
|
|
77
|
-
if (data.signal.name !== "navigation")
|
|
78
|
-
return;
|
|
79
|
-
const tab = import_tab.Tab.forPage(page);
|
|
80
|
-
const navigateAction = {
|
|
81
|
-
name: "navigate",
|
|
82
|
-
url: data.signal.url,
|
|
83
|
-
signals: []
|
|
84
|
-
};
|
|
85
|
-
if (tab)
|
|
86
|
-
sessionLog.logUserAction(navigateAction, tab, `await page.goto('${data.signal.url}');`, false);
|
|
87
|
-
}
|
|
88
|
-
});
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
|
|
92
43
|
class Context {
|
|
93
44
|
constructor(options) {
|
|
94
45
|
this._tabs = [];
|
|
@@ -300,9 +251,6 @@ class Context {
|
|
|
300
251
|
_live: true
|
|
301
252
|
});
|
|
302
253
|
}
|
|
303
|
-
// Initialize InputRecorder for user action capture
|
|
304
|
-
if (this.sessionLog)
|
|
305
|
-
await InputRecorder.create(this, browserContext);
|
|
306
254
|
return result;
|
|
307
255
|
}
|
|
308
256
|
lookupSecret(secretName) {
|
|
@@ -33,24 +33,8 @@ __export(sessionLog_exports, {
|
|
|
33
33
|
module.exports = __toCommonJS(sessionLog_exports);
|
|
34
34
|
var import_fs = __toESM(require("fs"));
|
|
35
35
|
var import_path = __toESM(require("path"));
|
|
36
|
-
var import_net = require("net");
|
|
37
36
|
var import_config = require("./config");
|
|
38
37
|
var import_response = require("./response");
|
|
39
|
-
function sendRecorderEvent(event) {
|
|
40
|
-
const port = process.env.CLAUDE_RECORDER_PORT;
|
|
41
|
-
if (!port)
|
|
42
|
-
return;
|
|
43
|
-
try {
|
|
44
|
-
const socket = import_net.createConnection({
|
|
45
|
-
port: Number(port),
|
|
46
|
-
host: process.env.CLAUDE_RECORDER_HOST || "127.0.0.1"
|
|
47
|
-
});
|
|
48
|
-
const payload = JSON.stringify(event) + "\n";
|
|
49
|
-
socket.on("error", () => socket.destroy());
|
|
50
|
-
socket.write(payload, () => socket.end());
|
|
51
|
-
} catch (error) {
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
38
|
class SessionLog {
|
|
55
39
|
constructor(sessionFolder) {
|
|
56
40
|
this._sessionFileQueue = Promise.resolve();
|
|
@@ -84,40 +68,6 @@ class SessionLog {
|
|
|
84
68
|
lines.push("");
|
|
85
69
|
this._sessionFileQueue = this._sessionFileQueue.then(() => import_fs.default.promises.appendFile(this._file, lines.join("\n")));
|
|
86
70
|
}
|
|
87
|
-
logUserAction(action, tab, code, isUpdate) {
|
|
88
|
-
code = code.trim();
|
|
89
|
-
// Send recorder event for user action tracking
|
|
90
|
-
const actionForLog = { ...action };
|
|
91
|
-
delete actionForLog.ariaSnapshot;
|
|
92
|
-
delete actionForLog.selector;
|
|
93
|
-
actionForLog.isUpdate = !!isUpdate;
|
|
94
|
-
sendRecorderEvent({
|
|
95
|
-
type: "user-action",
|
|
96
|
-
action: actionForLog,
|
|
97
|
-
code,
|
|
98
|
-
url: tab.page.url(),
|
|
99
|
-
timestamp: Date.now()
|
|
100
|
-
});
|
|
101
|
-
// Also log to session file
|
|
102
|
-
const lines = [""];
|
|
103
|
-
lines.push(
|
|
104
|
-
`### User action: ${action.name}`,
|
|
105
|
-
"- Args",
|
|
106
|
-
"```json",
|
|
107
|
-
JSON.stringify(actionForLog, null, 2),
|
|
108
|
-
"```"
|
|
109
|
-
);
|
|
110
|
-
if (code) {
|
|
111
|
-
lines.push(
|
|
112
|
-
"- Code",
|
|
113
|
-
"```js",
|
|
114
|
-
code,
|
|
115
|
-
"```"
|
|
116
|
-
);
|
|
117
|
-
}
|
|
118
|
-
lines.push("");
|
|
119
|
-
this._sessionFileQueue = this._sessionFileQueue.then(() => import_fs.default.promises.appendFile(this._file, lines.join("\n")));
|
|
120
|
-
}
|
|
121
71
|
}
|
|
122
72
|
// Annotate the CommonJS export names for ESM import in node:
|
|
123
73
|
0 && (module.exports = {
|
|
@@ -32,14 +32,9 @@ function createCustomMessageHandler(testInfo, context) {
|
|
|
32
32
|
if (data.initialize) {
|
|
33
33
|
if (backend)
|
|
34
34
|
throw new Error("MCP backend is already initialized");
|
|
35
|
-
backend = new import_browserServerBackend.BrowserServerBackend({ ...import_config.defaultConfig, capabilities: ["testing"]
|
|
35
|
+
backend = new import_browserServerBackend.BrowserServerBackend({ ...import_config.defaultConfig, capabilities: ["testing"] }, (0, import_browserContextFactory.identityBrowserContextFactory)(context));
|
|
36
36
|
await backend.initialize(data.initialize.clientInfo);
|
|
37
37
|
const pausedMessage = await generatePausedMessage(testInfo, context);
|
|
38
|
-
// Ensure recorder UI appears immediately by triggering a benign action that doesn't change page state.
|
|
39
|
-
try {
|
|
40
|
-
await backend.callTool("browser_tabs", { action: "list" });
|
|
41
|
-
} catch (e) {
|
|
42
|
-
}
|
|
43
38
|
return { initialize: { pausedMessage } };
|
|
44
39
|
}
|
|
45
40
|
if (data.listTools) {
|
package/dist/stably-browser.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
// ../../app/node_modules/.pnpm/@stablyai-internal+playwright-cli@0.4.
|
|
3
|
+
// ../../app/node_modules/.pnpm/@stablyai-internal+playwright-cli@0.4.16/node_modules/@stablyai-internal/playwright-cli/playwright-cli.js
|
|
4
4
|
var fs = require("fs");
|
|
5
5
|
var path = require("path");
|
|
6
6
|
var argv = process.argv.slice(2);
|
|
@@ -193,24 +193,51 @@ const test = realPw.test.extend({
|
|
|
193
193
|
|
|
194
194
|
page: async ({ context }, use) => {
|
|
195
195
|
const page = context.pages()[0] || await context.newPage();
|
|
196
|
-
//
|
|
197
|
-
//
|
|
198
|
-
//
|
|
196
|
+
// Resize the browser window to match the user's configured viewport.
|
|
197
|
+
// Strategy: resize the OS window via Browser.setWindowBounds when the viewport fits
|
|
198
|
+
// on screen (no device emulation needed). When it doesn't fit, maximize the window
|
|
199
|
+
// and use setViewportSize (device emulation) to force the exact dimensions \u2014 this is
|
|
200
|
+
// safe because the emulated viewport is larger than the window, so no white borders.
|
|
199
201
|
const vp = process.env.PLAYWRIGHT_CLI_VIEWPORT;
|
|
200
202
|
if (vp) {
|
|
201
203
|
const [w, h] = vp.split(',').map(Number);
|
|
202
204
|
if (w && h) {
|
|
203
|
-
await page.setViewportSize({ width: w, height: h });
|
|
204
|
-
// Also resize the actual browser window to match.
|
|
205
|
-
// setViewportSize only changes the CSS layout viewport via DeviceMetricsOverride,
|
|
206
|
-
// leaving the OS window at its original size (visible as dark gray dead space).
|
|
207
205
|
try {
|
|
208
206
|
const cdpSession = await context.newCDPSession(page);
|
|
209
207
|
const { windowId } = await cdpSession.send('Browser.getWindowForTarget');
|
|
210
|
-
|
|
211
|
-
await
|
|
212
|
-
|
|
213
|
-
|
|
208
|
+
const { bounds: currentBounds } = await cdpSession.send('Browser.getWindowBounds', { windowId });
|
|
209
|
+
const screenSize = await page.evaluate(() => ({ width: screen.width, height: screen.height }));
|
|
210
|
+
// Quick check: if viewport won't fit with decorations (~100px width, ~200px height),
|
|
211
|
+
// skip measurement and go straight to maximize + device emulation.
|
|
212
|
+
// We can't reliably measure decorations when device emulation is active (from open)
|
|
213
|
+
// because window.innerWidth/Height returns the emulated dimensions, not real ones.
|
|
214
|
+
if (w + 100 > screenSize.width || h + 200 > screenSize.height) {
|
|
215
|
+
// Chrome requires fullscreen \u2192 normal \u2192 maximized (can't go directly).
|
|
216
|
+
if (currentBounds.windowState === 'fullscreen') {
|
|
217
|
+
await cdpSession.send('Browser.setWindowBounds', { windowId, bounds: { windowState: 'normal' } });
|
|
218
|
+
}
|
|
219
|
+
await cdpSession.send('Browser.setWindowBounds', { windowId, bounds: { windowState: 'maximized' } });
|
|
220
|
+
// Use Emulation.setDeviceMetricsOverride directly instead of page.setViewportSize()
|
|
221
|
+
// because setViewportSize also resizes the window, pushing it off-screen and hiding
|
|
222
|
+
// the tab bar and address bar.
|
|
223
|
+
await cdpSession.send('Emulation.setDeviceMetricsOverride', {
|
|
224
|
+
width: w, height: h, deviceScaleFactor: 0, mobile: false
|
|
225
|
+
});
|
|
226
|
+
} else {
|
|
227
|
+
// Viewport fits \u2014 un-maximize, measure decorations, resize window exactly.
|
|
228
|
+
if (currentBounds.windowState === 'maximized' || currentBounds.windowState === 'fullscreen') {
|
|
229
|
+
await cdpSession.send('Browser.setWindowBounds', { windowId, bounds: { windowState: 'normal' } });
|
|
230
|
+
}
|
|
231
|
+
// Clear any prior device emulation so innerWidth/Height reflect actual window size.
|
|
232
|
+
await cdpSession.send('Emulation.clearDeviceMetricsOverride').catch(() => {});
|
|
233
|
+
const { bounds: measuredBounds } = await cdpSession.send('Browser.getWindowBounds', { windowId });
|
|
234
|
+
const inner = await page.evaluate(() => ({ width: window.innerWidth, height: window.innerHeight }));
|
|
235
|
+
const decoW = measuredBounds.width - inner.width;
|
|
236
|
+
const decoH = measuredBounds.height - inner.height;
|
|
237
|
+
const targetW = w + decoW;
|
|
238
|
+
const targetH = h + decoH;
|
|
239
|
+
await cdpSession.send('Browser.setWindowBounds', { windowId, bounds: { width: targetW, height: targetH } });
|
|
240
|
+
}
|
|
214
241
|
await cdpSession.detach();
|
|
215
242
|
} catch (_) { /* best-effort \u2014 headed only, fails silently in headless */ }
|
|
216
243
|
}
|
|
@@ -322,30 +349,28 @@ var _runArgs;
|
|
|
322
349
|
var _ptPkg;
|
|
323
350
|
var _ptCli;
|
|
324
351
|
if (cmdIndex !== -1 && argv[cmdIndex] === "open") {
|
|
352
|
+
process.env.DEBUG = (process.env.DEBUG || "") + (process.env.DEBUG ? ",pw:daemon" : "pw:daemon");
|
|
325
353
|
const cwd = process.cwd();
|
|
326
354
|
for (const name of ["playwright.config.ts", "playwright.config.js", "playwright.config.mjs"]) {
|
|
327
355
|
const configPath = path.join(cwd, name);
|
|
328
356
|
if (fs.existsSync(configPath)) {
|
|
329
357
|
try {
|
|
330
|
-
const {
|
|
331
|
-
const
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
const result =
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
env: { ...process.env, NODE_OPTIONS: "" }
|
|
347
|
-
}
|
|
348
|
-
).trim();
|
|
358
|
+
const { execFileSync } = require("child_process");
|
|
359
|
+
const extractCode = `
|
|
360
|
+
const { requireOrImport } = require('playwright/lib/transform/transform');
|
|
361
|
+
requireOrImport('${configPath.replace(/\\/g, "/").replace(/'/g, "\\'")}').then(m => {
|
|
362
|
+
const c = m.default || m;
|
|
363
|
+
const vp = c.use?.viewport || (c.projects || [])[0]?.use?.viewport;
|
|
364
|
+
if (vp && vp.width && vp.height) process.stdout.write(vp.width + 'x' + vp.height);
|
|
365
|
+
}).catch(() => {});
|
|
366
|
+
`;
|
|
367
|
+
const result = execFileSync(process.execPath, ["-e", extractCode], {
|
|
368
|
+
cwd,
|
|
369
|
+
encoding: "utf-8",
|
|
370
|
+
timeout: 1e4,
|
|
371
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
372
|
+
env: { ...process.env, NODE_OPTIONS: "" }
|
|
373
|
+
}).trim();
|
|
349
374
|
if (result && /^\d+x\d+$/.test(result)) {
|
|
350
375
|
process.env.PLAYWRIGHT_MCP_VIEWPORT_SIZE = result;
|
|
351
376
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "stably",
|
|
3
|
-
"version": "4.12.
|
|
3
|
+
"version": "4.12.5",
|
|
4
4
|
"packageManager": "pnpm@10.24.0",
|
|
5
5
|
"description": "AI-powered E2E Playwright testing CLI. Stably can understand your codebase, edit/run tests, and handle complex test scenarios for you.",
|
|
6
6
|
"main": "dist/index.mjs",
|
|
@@ -87,7 +87,7 @@
|
|
|
87
87
|
"playwright": "1.59.0-alpha-1771104257000",
|
|
88
88
|
"@stablyai/codegen-agent-constants": "workspace:*",
|
|
89
89
|
"@stablyai-internal/api-client": "workspace:*",
|
|
90
|
-
"@stablyai-internal/playwright-cli": "0.4.
|
|
90
|
+
"@stablyai-internal/playwright-cli": "0.4.16",
|
|
91
91
|
"@stablyai/agent-hooks": "workspace:*",
|
|
92
92
|
"@stablyai/agent-schemas": "workspace:*",
|
|
93
93
|
"@stablyai/agent-security-hooks": "workspace:*",
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
--- lib/mcp/browser/config.js
|
|
2
|
-
+++ lib/mcp/browser/config.js
|
|
3
|
-
@@ -305,7 +305,7 @@
|
|
4
|
-
function outputDir(config, clientInfo) {
|
|
5
|
-
if (config.outputDir)
|
|
6
|
-
return import_path.default.resolve(config.outputDir);
|
|
7
|
-
const rootPath = (0, import_server2.firstRootPath)(clientInfo);
|
|
8
|
-
if (rootPath)
|
|
9
|
-
- return import_path.default.resolve(rootPath, config.skillMode ? ".playwright-cli" : ".playwright-mcp");
|
|
10
|
-
+ return import_path.default.resolve(rootPath, config.skillMode ? ".stably-browser" : ".playwright-mcp");
|
|
11
|
-
const tmpDir = process.env.PW_TMPDIR_FOR_TEST ?? import_os.default.tmpdir();
|