stably 4.12.11 → 4.12.12

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.
@@ -118,40 +118,6 @@ class SessionLog {
118
118
  lines.push("");
119
119
  this._sessionFileQueue = this._sessionFileQueue.then(() => import_fs.default.promises.appendFile(this._file, lines.join("\n")));
120
120
  }
121
- logUserAction(action, tab, code, isUpdate) {
122
- code = code.trim();
123
- // Send recorder event for user action tracking
124
- const actionForLog = { ...action };
125
- delete actionForLog.ariaSnapshot;
126
- delete actionForLog.selector;
127
- actionForLog.isUpdate = !!isUpdate;
128
- sendRecorderEvent({
129
- type: "user-action",
130
- action: actionForLog,
131
- code,
132
- url: tab.page.url(),
133
- timestamp: Date.now()
134
- });
135
- // Also log to session file
136
- const lines = [""];
137
- lines.push(
138
- `### User action: ${action.name}`,
139
- "- Args",
140
- "```json",
141
- JSON.stringify(actionForLog, null, 2),
142
- "```"
143
- );
144
- if (code) {
145
- lines.push(
146
- "- Code",
147
- "```js",
148
- code,
149
- "```"
150
- );
151
- }
152
- lines.push("");
153
- this._sessionFileQueue = this._sessionFileQueue.then(() => import_fs.default.promises.appendFile(this._file, lines.join("\n")));
154
- }
155
121
  }
156
122
  // Annotate the CommonJS export names for ESM import in node:
157
123
  0 && (module.exports = {
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- // ../../app/node_modules/.pnpm/@stablyai-internal+playwright-cli@0.4.21/node_modules/@stablyai-internal/playwright-cli/playwright-cli.js
3
+ // ../../app/node_modules/.pnpm/@stablyai-internal+playwright-cli@0.4.22/node_modules/@stablyai-internal/playwright-cli/playwright-cli.js
4
4
  var fs = require("fs");
5
5
  var path = require("path");
6
6
  if (process.platform === "darwin" && !process.env.PLAYWRIGHT_DAEMON_SOCKETS_DIR) {
@@ -182,25 +182,134 @@ const userConfig = userConfigModule.default || userConfigModule;`;
182
182
  const fixturesCode = `
183
183
  // Auto-generated by stably-browser run-test
184
184
  // Overrides browser/context/page fixtures to connect via CDP and preserve state.
185
+ //
186
+ // WHY THIS EXISTS:
187
+ // When run-test runs a user's Playwright tests through CDP with project dependencies like:
188
+ // auth \u2192 app (app depends on auth completing first)
189
+ //
190
+ // The auth project may run sequential tests (e.g. "authenticate US" then "authenticate EU").
191
+ // Both are test-scoped, meaning Playwright tears down context/page fixtures between them.
192
+ // In CDP mode, fixtures reuse the browser's default context via browser.contexts()[0].
193
+ //
194
+ // THE BUG (before this fix):
195
+ // After the first auth test completes, the CDP default context can become invalid.
196
+ // When the second auth test's context fixture runs:
197
+ // 1. browser.contexts()[0] \u2192 undefined (context gone)
198
+ // 2. browser.newContext() \u2192 throws "Target page, context or browser has been closed"
199
+ // 3. This error kills the Chrome process (CDP port becomes unreachable)
200
+ // 4. All retries fail with ECONNREFUSED, all downstream tests fail
201
+ //
202
+ // The second auth test WOULD have called setup.skip() in its body, but fixtures
203
+ // are set up BEFORE the test body runs, so the skip never executes.
204
+ //
205
+ // THE FIX:
206
+ // Wrap the browser in a Proxy that catches CDP errors and reconnects.
207
+ // Context and page fixtures also catch errors and reconnect.
208
+ // This lets the second auth test get a valid context \u2192 run its body \u2192 call skip().
209
+
185
210
  const realPw = require(${JSON.stringify(realPwTestPath)});
186
211
 
212
+ const cdpEndpoint = process.env.PLAYWRIGHT_CDP_ENDPOINT;
213
+ if (!cdpEndpoint) throw new Error('PLAYWRIGHT_CDP_ENDPOINT not set');
214
+
215
+ // Shared mutable state: holds the current CDP browser connection.
216
+ // When a connection goes stale, reconnectBrowser() replaces it.
217
+ const cdpState = { browser: null };
218
+
219
+ async function connectBrowser(playwright) {
220
+ cdpState.browser = await playwright.chromium.connectOverCDP(cdpEndpoint);
221
+ return cdpState.browser;
222
+ }
223
+
224
+ // Replace the current CDP connection with a fresh one.
225
+ // Closes the old connection (best-effort) to avoid leaked handles.
226
+ async function reconnectBrowser(playwright) {
227
+ const previousBrowser = cdpState.browser;
228
+ const nextBrowser = await connectBrowser(playwright);
229
+ if (previousBrowser && previousBrowser !== nextBrowser) {
230
+ try {
231
+ await previousBrowser.close();
232
+ } catch (_) {
233
+ // Ignore disconnect errors from the stale CDP connection we are replacing.
234
+ }
235
+ }
236
+ return nextBrowser;
237
+ }
238
+
239
+ // Detect errors that indicate the CDP connection is stale but Chrome may still be alive.
240
+ function isRecoverableCdpError(error) {
241
+ return !!error && typeof error.message === 'string' &&
242
+ (error.message.includes('Target page, context or browser has been closed') ||
243
+ error.message.includes('Browser has been closed') ||
244
+ error.message.includes('Connection closed'));
245
+ }
246
+
247
+ // Wrap the browser object in a Proxy so that calls like browser.newContext()
248
+ // automatically retry with a fresh CDP connection on recoverable errors.
249
+ // This is transparent to callers \u2014 they get back a working context.
250
+ function createReconnectableBrowserProxy(playwright) {
251
+ return new Proxy({}, {
252
+ get(_target, prop) {
253
+ const activeBrowser = cdpState.browser;
254
+ const value = activeBrowser?.[prop];
255
+ if (prop === 'newContext' && typeof value === 'function') {
256
+ return async (...args) => {
257
+ try {
258
+ return await value.apply(activeBrowser, args);
259
+ } catch (error) {
260
+ if (!isRecoverableCdpError(error)) throw error;
261
+ const freshBrowser = await reconnectBrowser(playwright);
262
+ return freshBrowser.newContext(...args);
263
+ }
264
+ };
265
+ }
266
+ return typeof value === 'function' ? value.bind(activeBrowser) : value;
267
+ },
268
+ });
269
+ }
270
+
187
271
  const test = realPw.test.extend({
272
+ // Worker-scoped: one CDP connection per Playwright worker.
273
+ // Wrapped in a Proxy so newContext() auto-recovers from stale connections.
188
274
  browser: [async ({ playwright }, use) => {
189
- const cdpEndpoint = process.env.PLAYWRIGHT_CDP_ENDPOINT;
190
- if (!cdpEndpoint) throw new Error('PLAYWRIGHT_CDP_ENDPOINT not set');
191
- const browser = await playwright.chromium.connectOverCDP(cdpEndpoint);
192
- await use(browser);
275
+ await connectBrowser(playwright);
276
+ await use(createReconnectableBrowserProxy(playwright));
193
277
  // No close \u2014 daemon needs the connection alive
194
278
  }, { scope: 'worker', timeout: 0 }],
195
279
 
196
- context: async ({ browser }, use) => {
197
- const context = browser.contexts()[0] || await browser.newContext();
280
+ // Test-scoped: re-acquired per test from the shared browser.
281
+ // After auth test #1 tears down, the default context may be gone.
282
+ // The try/catch catches the "Target page, context or browser has been closed" error
283
+ // and reconnects before the test body (which would call setup.skip()) runs.
284
+ context: async ({ browser, playwright }, use) => {
285
+ let context = browser.contexts()[0];
286
+ if (!context) {
287
+ try {
288
+ context = await browser.newContext();
289
+ } catch (error) {
290
+ if (!isRecoverableCdpError(error)) throw error;
291
+ const freshBrowser = await reconnectBrowser(playwright);
292
+ context = freshBrowser.contexts()[0] || await freshBrowser.newContext();
293
+ }
294
+ }
198
295
  await use(context);
199
296
  // No close \u2014 preserve state for post-test inspection
200
297
  },
201
298
 
202
- page: async ({ context }, use) => {
203
- const page = context.pages()[0] || await context.newPage();
299
+ // Test-scoped: re-acquired per test from the current context.
300
+ // Same recovery pattern as the context fixture.
301
+ page: async ({ context, browser, playwright }, use) => {
302
+ let page = context.pages()[0];
303
+ if (!page) {
304
+ try {
305
+ page = await context.newPage();
306
+ } catch (error) {
307
+ if (!isRecoverableCdpError(error)) throw error;
308
+ const freshBrowser = await reconnectBrowser(playwright);
309
+ const ctx = freshBrowser.contexts()[0] || await freshBrowser.newContext();
310
+ page = ctx.pages()[0] || await ctx.newPage();
311
+ }
312
+ }
204
313
  // Clear any stale device emulation left by a prior test's page.setViewportSize() call.
205
314
  // setViewportSize sets Emulation.setDeviceMetricsOverride which persists across run-test
206
315
  // invocations on the same daemon. Without clearing, window.innerWidth returns the emulated
@@ -274,11 +383,22 @@ module.exports = { ...realPw, test, default: test };
274
383
  // Auto-generated by stably-browser run-test
275
384
  // Intercepts require('@playwright/test') to use CDP-aware fixtures.
276
385
  const Module = require('module');
386
+ const path = require('path');
277
387
  const originalResolve = Module._resolveFilename;
278
388
  const fixturesPath = ${JSON.stringify(fixturesPath)};
279
389
 
390
+ // Only redirect require('@playwright/test') from USER test files to our CDP fixtures.
391
+ // Without this guard, Playwright's own internal require('@playwright/test') calls
392
+ // would also get redirected, breaking Playwright's internals. We check parent.filename
393
+ // to let anything inside node_modules/playwright/ or node_modules/@playwright/ resolve
394
+ // to the real module.
395
+ function isPlaywrightInternal(filename) {
396
+ const normalizedFilename = filename.split(path.sep).join('/');
397
+ return normalizedFilename.includes('/node_modules/playwright/') || normalizedFilename.includes('/node_modules/@playwright/');
398
+ }
399
+
280
400
  Module._resolveFilename = function(request, parent, isMain, options) {
281
- if (request === '@playwright/test') {
401
+ if (request === '@playwright/test' && (!parent || !parent.filename || !isPlaywrightInternal(parent.filename))) {
282
402
  return fixturesPath;
283
403
  }
284
404
  return originalResolve.call(this, request, parent, isMain, options);
@@ -398,4 +518,12 @@ if (cmdIndex !== -1 && argv[cmdIndex] === "open") {
398
518
  }
399
519
  }
400
520
  }
521
+ if (!process.env.PLAYWRIGHT_MCP_CONFIG) {
522
+ const os = require("os");
523
+ const shmFixConfigPath = path.join(os.tmpdir(), "playwright-cli-shm-fix.json");
524
+ fs.writeFileSync(shmFixConfigPath, JSON.stringify({
525
+ browser: { launchOptions: { args: ["--disable-dev-shm-usage"] } }
526
+ }));
527
+ process.env.PLAYWRIGHT_MCP_CONFIG = shmFixConfigPath;
528
+ }
401
529
  require("playwright/lib/cli/client/program");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "stably",
3
- "version": "4.12.11",
3
+ "version": "4.12.12",
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.21",
90
+ "@stablyai-internal/playwright-cli": "0.4.22",
91
91
  "@stablyai-internal/pwtrace": "0.3.1",
92
92
  "@stablyai/agent-hooks": "workspace:*",
93
93
  "@stablyai/agent-schemas": "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();