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 show chrome (tabs, URL bar).
79
- // Without this, the remote browser's viewport fills the entire window, hiding chrome.
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
- const pages = browserContext.pages();
83
- const page = pages[0];
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
- const CHROME_HEADER_HEIGHT = 85;
111
- await cdpSession.send("Browser.setWindowBounds", {
112
- windowId,
113
- bounds: { width: vp.width, height: vp.height + CHROME_HEADER_HEIGHT }
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);
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- // ../../app/node_modules/.pnpm/@stablyai-internal+playwright-cli@0.4.6/node_modules/@stablyai-internal/playwright-cli/playwright-cli.js
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
- // Apply viewport from user's playwright.config.ts if specified.
197
- // The daemon's context was created with viewport: null (browser window size),
198
- // so we need to explicitly set it to match the user's config.
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
- // Add offset for browser chrome (tab bar + address bar, ~85px on macOS)
211
- await cdpSession.send('Browser.setWindowBounds', {
212
- windowId, bounds: { width: w, height: h + 85 }
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 { execSync } = require("child_process");
331
- const isTS = name.endsWith(".ts");
332
- const extractCode = [
333
- `const m = require('${configPath.replace(/\\/g, "/").replace(/'/g, "\\'")}');`,
334
- `const c = m.default || m;`,
335
- `const vp = c.use?.viewport || (c.projects || [])[0]?.use?.viewport;`,
336
- `if (vp && vp.width && vp.height) process.stdout.write(vp.width + 'x' + vp.height);`
337
- ].join(" ");
338
- const nodeArgs = isTS ? ["--require", "tsx/cjs"] : [];
339
- const result = execSync(
340
- [process.execPath, ...nodeArgs, "-e", extractCode].map((a) => `"${a}"`).join(" "),
341
- {
342
- cwd,
343
- encoding: "utf-8",
344
- timeout: 1e4,
345
- stdio: ["pipe", "pipe", "pipe"],
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.4",
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.6",
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:*",