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 = {
|
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.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
|
-
|
|
190
|
-
|
|
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
|
-
|
|
197
|
-
|
|
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
|
-
|
|
203
|
-
|
|
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.
|
|
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.
|
|
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();
|