stably 4.12.4 → 4.12.6
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.
|
@@ -75,13 +75,25 @@ 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
|
|
79
|
-
//
|
|
78
|
+
// For CDP connections: resize the browser window to match the user's configured viewport.
|
|
79
|
+
// We only use Browser.setWindowBounds (no setViewportSize) so the page viewport is
|
|
80
|
+
// determined naturally by the window's content area. Using setViewportSize would trigger
|
|
81
|
+
// Emulation.setDeviceMetricsOverride which creates a device emulation frame with white
|
|
82
|
+
// borders inside the window — the page renders at the viewport size but doesn't fill
|
|
83
|
+
// the window's content area.
|
|
80
84
|
if (config.browser.cdpEndpoint) {
|
|
81
85
|
try {
|
|
82
|
-
|
|
83
|
-
|
|
86
|
+
// Pages may not be immediately available after CDP connection — wait briefly for discovery.
|
|
87
|
+
let page = browserContext.pages()[0];
|
|
88
|
+
if (!page) {
|
|
89
|
+
daemonDebug("no pages found, waiting for page discovery...");
|
|
90
|
+
page = await Promise.race([
|
|
91
|
+
new Promise(resolve => browserContext.once("page", resolve)),
|
|
92
|
+
new Promise(resolve => setTimeout(() => resolve(null), 5000))
|
|
93
|
+
]);
|
|
94
|
+
}
|
|
84
95
|
if (page) {
|
|
96
|
+
daemonDebug("found page for viewport resize:", page.url());
|
|
85
97
|
// Try to load viewport from the user's playwright.config (most authoritative source),
|
|
86
98
|
// then fall back to config.browser.contextOptions.viewport (which may just be the 1280x720 default).
|
|
87
99
|
let viewport = null;
|
|
@@ -101,18 +113,62 @@ async function startMcpDaemonServer(config, contextFactory) {
|
|
|
101
113
|
if (!viewport) {
|
|
102
114
|
viewport = config.browser.contextOptions?.viewport;
|
|
103
115
|
}
|
|
104
|
-
if (viewport && viewport.width && viewport.height) {
|
|
105
|
-
await page.setViewportSize(viewport);
|
|
106
|
-
}
|
|
107
116
|
const cdpSession = await browserContext.newCDPSession(page);
|
|
108
117
|
const { windowId } = await cdpSession.send("Browser.getWindowForTarget");
|
|
109
118
|
const vp = viewport || { width: 1280, height: 720 };
|
|
110
|
-
|
|
111
|
-
await cdpSession.send("Browser.
|
|
112
|
-
|
|
113
|
-
|
|
119
|
+
// Get current window state and measure chrome decoration overhead.
|
|
120
|
+
const { bounds: currentBounds } = await cdpSession.send("Browser.getWindowBounds", { windowId });
|
|
121
|
+
daemonDebug("current window state:", currentBounds.windowState, "bounds:", currentBounds.width, "x", currentBounds.height);
|
|
122
|
+
// Get screen size to know if the desired viewport can fit.
|
|
123
|
+
const { result: screenSize } = await cdpSession.send("Runtime.evaluate", {
|
|
124
|
+
expression: "JSON.stringify({ width: screen.width, height: screen.height })",
|
|
125
|
+
returnByValue: true
|
|
114
126
|
});
|
|
127
|
+
const screen = JSON.parse(screenSize.value);
|
|
128
|
+
daemonDebug("screen size:", screen.width, "x", screen.height);
|
|
129
|
+
// Quick check: if viewport won't fit with decorations (~100px width, ~200px height),
|
|
130
|
+
// skip measurement and go straight to maximize + device emulation.
|
|
131
|
+
if (vp.width + 100 > screen.width || vp.height + 200 > screen.height) {
|
|
132
|
+
daemonDebug("viewport", vp.width, "x", vp.height, "exceeds screen, using maximize + device emulation");
|
|
133
|
+
// Chrome requires fullscreen → normal → maximized (can't go directly fullscreen → maximized).
|
|
134
|
+
if (currentBounds.windowState === "fullscreen") {
|
|
135
|
+
await cdpSession.send("Browser.setWindowBounds", { windowId, bounds: { windowState: "normal" } });
|
|
136
|
+
}
|
|
137
|
+
await cdpSession.send("Browser.setWindowBounds", { windowId, bounds: { windowState: "maximized" } });
|
|
138
|
+
// Use Emulation.setDeviceMetricsOverride directly instead of page.setViewportSize()
|
|
139
|
+
// because setViewportSize also resizes the window, pushing it off-screen and hiding
|
|
140
|
+
// the tab bar and address bar.
|
|
141
|
+
await cdpSession.send("Emulation.setDeviceMetricsOverride", {
|
|
142
|
+
width: vp.width, height: vp.height, deviceScaleFactor: 0, mobile: false
|
|
143
|
+
});
|
|
144
|
+
daemonDebug("device emulation applied:", vp.width, "x", vp.height);
|
|
145
|
+
} else {
|
|
146
|
+
// Viewport fits — un-maximize, measure decorations, resize window exactly.
|
|
147
|
+
if (currentBounds.windowState === "maximized" || currentBounds.windowState === "fullscreen") {
|
|
148
|
+
await cdpSession.send("Browser.setWindowBounds", { windowId, bounds: { windowState: "normal" } });
|
|
149
|
+
daemonDebug("un-maximized window for measurement");
|
|
150
|
+
}
|
|
151
|
+
const { bounds: measuredBounds } = await cdpSession.send("Browser.getWindowBounds", { windowId });
|
|
152
|
+
const { result: innerSize } = await cdpSession.send("Runtime.evaluate", {
|
|
153
|
+
expression: "JSON.stringify({ width: window.innerWidth, height: window.innerHeight })",
|
|
154
|
+
returnByValue: true
|
|
155
|
+
});
|
|
156
|
+
const inner = JSON.parse(innerSize.value);
|
|
157
|
+
const decorationWidth = measuredBounds.width - inner.width;
|
|
158
|
+
const decorationHeight = measuredBounds.height - inner.height;
|
|
159
|
+
daemonDebug("measured chrome decorations:", decorationWidth, "x", decorationHeight, "(window:", measuredBounds.width, "x", measuredBounds.height, "inner:", inner.width, "x", inner.height, ")");
|
|
160
|
+
const targetWidth = vp.width + decorationWidth;
|
|
161
|
+
const targetHeight = vp.height + decorationHeight;
|
|
162
|
+
daemonDebug("resizing window to:", targetWidth, "x", targetHeight, "for viewport:", vp.width, "x", vp.height);
|
|
163
|
+
await cdpSession.send("Browser.setWindowBounds", {
|
|
164
|
+
windowId,
|
|
165
|
+
bounds: { width: targetWidth, height: targetHeight }
|
|
166
|
+
});
|
|
167
|
+
daemonDebug("window resize succeeded");
|
|
168
|
+
}
|
|
115
169
|
await cdpSession.detach();
|
|
170
|
+
} else {
|
|
171
|
+
daemonDebug("no page available for viewport resize after waiting");
|
|
116
172
|
}
|
|
117
173
|
} catch (e) {
|
|
118
174
|
daemonDebug("failed to resize window for CDP connection (best-effort)", e);
|
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.6",
|
|
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:*",
|