stably 4.12.2 → 4.12.4
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 +43 -0
- package/dist/node_modules/playwright/lib/index.js +32 -1
- package/dist/node_modules/playwright/lib/mcp/browser/browserServerBackend.js +27 -0
- package/dist/node_modules/playwright/lib/mcp/browser/config.js.rej +11 -0
- package/dist/node_modules/playwright/lib/mcp/browser/context.js +52 -0
- package/dist/node_modules/playwright/lib/mcp/browser/sessionLog.js +50 -0
- package/dist/node_modules/playwright/lib/mcp/test/browserBackend.js +6 -1
- package/dist/{playwright-cli.js → stably-browser.js} +22 -14
- package/dist/stably-plugin-cli/skills/browser-interaction-guide/SKILL.md +30 -30
- package/dist/stably-plugin-cli/skills/bulk-test-handling/SKILL.md +1 -1
- package/dist/stably-plugin-cli/skills/debugging-test-failures/SKILL.md +11 -11
- package/dist/stably-plugin-cli/skills/playwright-best-practices/SKILL.md +6 -6
- package/dist/stably-plugin-cli/skills/playwright-config-auth/SKILL.md +8 -8
- package/dist/stably-plugin-cli/skills/{playwright-cli → stably-browser}/SKILL.md +156 -156
- package/dist/stably-plugin-cli/skills/{playwright-cli → stably-browser}/references/request-mocking.md +11 -11
- package/dist/stably-plugin-cli/skills/{playwright-cli → stably-browser}/references/running-code.md +28 -28
- package/dist/stably-plugin-cli/skills/{playwright-cli → stably-browser}/references/session-management.md +40 -40
- package/dist/stably-plugin-cli/skills/{playwright-cli → stably-browser}/references/storage-state.md +41 -41
- package/dist/stably-plugin-cli/skills/{playwright-cli → stably-browser}/references/test-generation.md +10 -10
- package/dist/stably-plugin-cli/skills/{playwright-cli → stably-browser}/references/tracing.md +23 -23
- package/dist/stably-plugin-cli/skills/{playwright-cli → stably-browser}/references/video-recording.md +8 -8
- package/dist/stably-plugin-cli/skills/test-creation-workflow/SKILL.md +15 -15
- package/package.json +2 -2
|
@@ -75,6 +75,49 @@ 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
|
+
}
|
|
78
121
|
browserContext.on("close", () => {
|
|
79
122
|
daemonDebug("browser closed, shutting down daemon");
|
|
80
123
|
shutdown(0);
|
|
@@ -92,6 +92,21 @@ 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
|
+
}
|
|
95
110
|
if (connectOptions) {
|
|
96
111
|
const browser2 = await playwright[browserName].connect({
|
|
97
112
|
...connectOptions,
|
|
@@ -335,7 +350,13 @@ const playwrightFixtures = {
|
|
|
335
350
|
size: typeof video === "string" ? void 0 : video.size
|
|
336
351
|
}
|
|
337
352
|
} : {};
|
|
338
|
-
|
|
353
|
+
// When connected via CDP (run-test), reuse the default context instead of
|
|
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 });
|
|
339
360
|
if (process.env.PW_CLOCK === "frozen") {
|
|
340
361
|
await context._wrapApiCall(async () => {
|
|
341
362
|
await context.clock.install({ time: 0 });
|
|
@@ -351,6 +372,10 @@ const playwrightFixtures = {
|
|
|
351
372
|
if (closed)
|
|
352
373
|
return;
|
|
353
374
|
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;
|
|
354
379
|
const closeReason = testInfo.status === "timedOut" ? "Test timeout of " + testInfo.timeout + "ms exceeded." : "Test ended.";
|
|
355
380
|
await context.close({ reason: closeReason });
|
|
356
381
|
const testFailed = testInfo.status !== testInfo.expectedStatus;
|
|
@@ -403,6 +428,12 @@ const playwrightFixtures = {
|
|
|
403
428
|
await browserImpl._wrapApiCall(() => browserImpl._disconnectFromReusedContext(closeReason), { internal: true });
|
|
404
429
|
},
|
|
405
430
|
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
|
+
}
|
|
406
437
|
if (!_reuseContext) {
|
|
407
438
|
await use(await context.newPage());
|
|
408
439
|
return;
|
|
@@ -27,6 +27,22 @@ 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
|
+
}
|
|
30
46
|
class BrowserServerBackend {
|
|
31
47
|
constructor(config, factory, options = {}) {
|
|
32
48
|
this._config = config;
|
|
@@ -64,6 +80,17 @@ Tool "${name}" not found` }],
|
|
|
64
80
|
await tool.handle(context, parsedArguments, response);
|
|
65
81
|
responseObject = await response.serialize();
|
|
66
82
|
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
|
+
}
|
|
67
94
|
} catch (error) {
|
|
68
95
|
return {
|
|
69
96
|
content: [{ type: "text", text: `### Error
|
|
@@ -0,0 +1,11 @@
|
|
|
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();
|
|
@@ -40,6 +40,55 @@ 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
|
+
|
|
43
92
|
class Context {
|
|
44
93
|
constructor(options) {
|
|
45
94
|
this._tabs = [];
|
|
@@ -251,6 +300,9 @@ class Context {
|
|
|
251
300
|
_live: true
|
|
252
301
|
});
|
|
253
302
|
}
|
|
303
|
+
// Initialize InputRecorder for user action capture
|
|
304
|
+
if (this.sessionLog)
|
|
305
|
+
await InputRecorder.create(this, browserContext);
|
|
254
306
|
return result;
|
|
255
307
|
}
|
|
256
308
|
lookupSecret(secretName) {
|
|
@@ -33,8 +33,24 @@ __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");
|
|
36
37
|
var import_config = require("./config");
|
|
37
38
|
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
|
+
}
|
|
38
54
|
class SessionLog {
|
|
39
55
|
constructor(sessionFolder) {
|
|
40
56
|
this._sessionFileQueue = Promise.resolve();
|
|
@@ -68,6 +84,40 @@ class SessionLog {
|
|
|
68
84
|
lines.push("");
|
|
69
85
|
this._sessionFileQueue = this._sessionFileQueue.then(() => import_fs.default.promises.appendFile(this._file, lines.join("\n")));
|
|
70
86
|
}
|
|
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
|
+
}
|
|
71
121
|
}
|
|
72
122
|
// Annotate the CommonJS export names for ESM import in node:
|
|
73
123
|
0 && (module.exports = {
|
|
@@ -32,9 +32,14 @@ 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"] }, (0, import_browserContextFactory.identityBrowserContextFactory)(context));
|
|
35
|
+
backend = new import_browserServerBackend.BrowserServerBackend({ ...import_config.defaultConfig, capabilities: ["testing"], saveSession: true }, (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
|
+
}
|
|
38
43
|
return { initialize: { pausedMessage } };
|
|
39
44
|
}
|
|
40
45
|
if (data.listTools) {
|
|
@@ -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.6/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);
|
|
@@ -8,7 +8,7 @@ var cmdIndex = argv.findIndex((arg) => !arg.startsWith("-"));
|
|
|
8
8
|
if (cmdIndex !== -1 && argv[cmdIndex] === "run-file") {
|
|
9
9
|
const fileArg = argv[cmdIndex + 1];
|
|
10
10
|
if (!fileArg) {
|
|
11
|
-
console.error("Usage:
|
|
11
|
+
console.error("Usage: stably-browser run-file <path>");
|
|
12
12
|
process.exit(1);
|
|
13
13
|
}
|
|
14
14
|
const filePath = path.resolve(fileArg);
|
|
@@ -23,7 +23,7 @@ if (cmdIndex !== -1 && argv[cmdIndex] === "run-file") {
|
|
|
23
23
|
}
|
|
24
24
|
if (cmdIndex !== -1 && argv[cmdIndex] === "run-test") {
|
|
25
25
|
if (argv.includes("--help") || argv.includes("-h")) {
|
|
26
|
-
console.log(`Usage:
|
|
26
|
+
console.log(`Usage: stably-browser run-test [test-filter] [options]
|
|
27
27
|
|
|
28
28
|
Runs Playwright tests connected to the managed browser via CDP.
|
|
29
29
|
Tests connect to the existing browser session instead of launching a new one.
|
|
@@ -147,7 +147,7 @@ Notes:
|
|
|
147
147
|
const cwd = process.cwd();
|
|
148
148
|
const wrapperConfigPath = path.join(
|
|
149
149
|
cwd,
|
|
150
|
-
`.
|
|
150
|
+
`.stably-browser-run-test-${crypto.randomUUID()}.config.js`
|
|
151
151
|
);
|
|
152
152
|
let userConfigImport = "";
|
|
153
153
|
for (const name of ["playwright.config.ts", "playwright.config.js", "playwright.config.mjs"]) {
|
|
@@ -161,7 +161,7 @@ const userConfig = userConfigModule.default || userConfigModule;`;
|
|
|
161
161
|
if (!userConfigImport) {
|
|
162
162
|
userConfigImport = `const userConfig = {};`;
|
|
163
163
|
}
|
|
164
|
-
const tmpDir = path.join(os.tmpdir(), `
|
|
164
|
+
const tmpDir = path.join(os.tmpdir(), `stably-browser-${crypto.randomUUID()}`);
|
|
165
165
|
fs.mkdirSync(tmpDir, { recursive: true });
|
|
166
166
|
let realPwTestPath;
|
|
167
167
|
try {
|
|
@@ -172,7 +172,7 @@ const userConfig = userConfigModule.default || userConfigModule;`;
|
|
|
172
172
|
}
|
|
173
173
|
const fixturesPath = path.join(tmpDir, "stably-cdp-fixtures.js");
|
|
174
174
|
const fixturesCode = `
|
|
175
|
-
// Auto-generated by
|
|
175
|
+
// Auto-generated by stably-browser run-test
|
|
176
176
|
// Overrides browser/context/page fixtures to connect via CDP and preserve state.
|
|
177
177
|
const realPw = require(${JSON.stringify(realPwTestPath)});
|
|
178
178
|
|
|
@@ -226,7 +226,7 @@ module.exports = { ...realPw, test, default: test };
|
|
|
226
226
|
fs.writeFileSync(fixturesPath, fixturesCode);
|
|
227
227
|
const shimPath = path.join(tmpDir, "stably-require-shim.js");
|
|
228
228
|
const shimCode = `
|
|
229
|
-
// Auto-generated by
|
|
229
|
+
// Auto-generated by stably-browser run-test
|
|
230
230
|
// Intercepts require('@playwright/test') to use CDP-aware fixtures.
|
|
231
231
|
const Module = require('module');
|
|
232
232
|
const originalResolve = Module._resolveFilename;
|
|
@@ -241,7 +241,7 @@ Module._resolveFilename = function(request, parent, isMain, options) {
|
|
|
241
241
|
`;
|
|
242
242
|
fs.writeFileSync(shimPath, shimCode);
|
|
243
243
|
const wrapperConfig = `
|
|
244
|
-
// Auto-generated by
|
|
244
|
+
// Auto-generated by stably-browser run-test \u2014 connects to daemon browser via CDP.
|
|
245
245
|
const { defineConfig } = require('@playwright/test');
|
|
246
246
|
${userConfigImport}
|
|
247
247
|
|
|
@@ -282,12 +282,16 @@ module.exports = defineConfig({
|
|
|
282
282
|
`;
|
|
283
283
|
fs.writeFileSync(wrapperConfigPath, wrapperConfig);
|
|
284
284
|
const testArgs = argv.slice(cmdIndex + 1).filter((a) => !a.startsWith("--config=") && !a.startsWith("--session=") && !a.startsWith("-s=") && a !== "--headed");
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
if (fs.existsSync(_ptCli)) {
|
|
290
|
-
|
|
285
|
+
_runBin = "npx", _runArgs = ["playwright", "test"];
|
|
286
|
+
try {
|
|
287
|
+
_ptPkg = require.resolve("@playwright/test/package.json", { paths: [cwd] });
|
|
288
|
+
_ptCli = path.join(path.dirname(_ptPkg), "cli.js");
|
|
289
|
+
if (fs.existsSync(_ptCli)) {
|
|
290
|
+
_runBin = process.execPath;
|
|
291
|
+
_runArgs = [_ptCli, "test"];
|
|
292
|
+
}
|
|
293
|
+
} catch (_) {
|
|
294
|
+
}
|
|
291
295
|
let exitCode = 0;
|
|
292
296
|
try {
|
|
293
297
|
execFileSync(_runBin, [..._runArgs, "--config=" + wrapperConfigPath, ...testArgs], {
|
|
@@ -313,6 +317,10 @@ module.exports = defineConfig({
|
|
|
313
317
|
}
|
|
314
318
|
process.exit(exitCode);
|
|
315
319
|
}
|
|
320
|
+
var _runBin;
|
|
321
|
+
var _runArgs;
|
|
322
|
+
var _ptPkg;
|
|
323
|
+
var _ptCli;
|
|
316
324
|
if (cmdIndex !== -1 && argv[cmdIndex] === "open") {
|
|
317
325
|
const cwd = process.cwd();
|
|
318
326
|
for (const name of ["playwright.config.ts", "playwright.config.js", "playwright.config.mjs"]) {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: browser-interaction-guide
|
|
3
|
-
description: Guide for browser interactions in Playwright tests including
|
|
3
|
+
description: Guide for browser interactions in Playwright tests including stably-browser commands, wait patterns, headless mode, locator best practices, and browser cleanup. Use when setting up browser sessions, writing wait patterns, fixing locator issues, or debugging headless failures. Triggers on browser setup, stably-browser, run-test, snapshot, wait pattern, waitFor, headless, locator, browser cleanup.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Browser Interaction Guide
|
|
@@ -9,47 +9,47 @@ Guide for browser interactions in Playwright tests.
|
|
|
9
9
|
|
|
10
10
|
## Browser Setup
|
|
11
11
|
|
|
12
|
-
Use `
|
|
12
|
+
Use `stably-browser` commands for browser interaction. The browser starts lazily on first command.
|
|
13
13
|
|
|
14
14
|
```bash
|
|
15
15
|
# Open a browser and navigate
|
|
16
|
-
|
|
16
|
+
stably-browser open https://example.com
|
|
17
17
|
|
|
18
18
|
# Or open then navigate
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
stably-browser open
|
|
20
|
+
stably-browser goto https://example.com
|
|
21
21
|
```
|
|
22
22
|
|
|
23
23
|
### Browser Persists After run-test
|
|
24
24
|
|
|
25
|
-
`
|
|
25
|
+
`stably-browser run-test` auto-starts a browser if needed and preserves the session after tests finish. You can immediately use `stably-browser snapshot`, `stably-browser click`, etc. to inspect page state. No need to run `stably-browser open` before or after `run-test`.
|
|
26
26
|
|
|
27
27
|
### Reset Browser Between Independent Test Verifications
|
|
28
28
|
|
|
29
|
-
**CRITICAL:** When verifying that multiple independent tests pass, you **MUST** run `
|
|
29
|
+
**CRITICAL:** When verifying that multiple independent tests pass, you **MUST** run `stably-browser close` between each `run-test` call. Browser state (cookies, localStorage, auth sessions) persists across runs, so a stale or logged-in state from one test will contaminate the next — causing cascading login failures or unexpected behavior.
|
|
30
30
|
|
|
31
|
-
**IMPORTANT: `
|
|
31
|
+
**IMPORTANT: `stably-browser close` must be its own separate Bash call.** Do NOT chain it with other commands (e.g., `stably-browser close; stably-browser run-test ...`). The browser is only fully terminated after the Bash command finishes, so chaining means the next command runs on the old browser with stale state.
|
|
32
32
|
|
|
33
33
|
This does NOT apply when intentionally using `run-test` as a setup/seed mechanism to build state for subsequent interaction.
|
|
34
34
|
|
|
35
35
|
```bash
|
|
36
36
|
# CORRECT: Each command is a separate Bash call
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
37
|
+
stably-browser run-test tests/test-a.spec.ts
|
|
38
|
+
stably-browser close
|
|
39
|
+
stably-browser run-test tests/test-b.spec.ts
|
|
40
|
+
stably-browser close
|
|
41
41
|
```
|
|
42
42
|
|
|
43
43
|
```bash
|
|
44
44
|
# WRONG: Chaining close with run-test in one command
|
|
45
|
-
|
|
46
|
-
|
|
45
|
+
stably-browser close; stably-browser run-test tests/test-b.spec.ts # ← run-test hits old browser
|
|
46
|
+
stably-browser close && stably-browser run-test tests/test-b.spec.ts # ← same problem
|
|
47
47
|
```
|
|
48
48
|
|
|
49
49
|
```bash
|
|
50
50
|
# WRONG: Running independent tests without resetting
|
|
51
|
-
|
|
52
|
-
|
|
51
|
+
stably-browser run-test tests/test-a.spec.ts
|
|
52
|
+
stably-browser run-test tests/test-b.spec.ts # ← may fail: stale state from test-a
|
|
53
53
|
```
|
|
54
54
|
|
|
55
55
|
### Tool Decision Tree
|
|
@@ -57,19 +57,19 @@ playwright-cli run-test tests/test-b.spec.ts # ← may fail: stale state from
|
|
|
57
57
|
```
|
|
58
58
|
What do you need to do?
|
|
59
59
|
├── Explore a page interactively
|
|
60
|
-
│ └──
|
|
60
|
+
│ └── stably-browser open <url>
|
|
61
61
|
│ Read snapshot from output → interact with refs directly
|
|
62
62
|
│
|
|
63
63
|
├── Run a test then explore (auth setup, state setup)
|
|
64
|
-
│ └──
|
|
64
|
+
│ └── stably-browser run-test <seedFile> --project=<project>
|
|
65
65
|
│ Then: click/fill/snapshot directly (browser stays open)
|
|
66
66
|
│
|
|
67
67
|
├── Debug a failing test
|
|
68
|
-
│ └──
|
|
69
|
-
│ Read output → inspect with
|
|
68
|
+
│ └── stably-browser run-test <file>
|
|
69
|
+
│ Read output → inspect with stably-browser snapshot if needed
|
|
70
70
|
│
|
|
71
71
|
└── Verify a test passes
|
|
72
|
-
└──
|
|
72
|
+
└── stably-browser run-test <file>
|
|
73
73
|
```
|
|
74
74
|
|
|
75
75
|
For project selection (auth dependencies), see `playwright-config-auth` skill.
|
|
@@ -78,13 +78,13 @@ For project selection (auth dependencies), see `playwright-config-auth` skill.
|
|
|
78
78
|
|
|
79
79
|
| Error Type | Action |
|
|
80
80
|
|------------|--------|
|
|
81
|
-
| **Test code error** (assertion failed, locator not found) | Test the locator interactively with `
|
|
81
|
+
| **Test code error** (assertion failed, locator not found) | Test the locator interactively with `stably-browser run-code` first (seconds vs minutes), then fix code and re-run with `stably-browser run-test` |
|
|
82
82
|
| **External service error** (timeout, network) | Retry up to 3 times |
|
|
83
83
|
| **Same error 3+ times** | Flag as blocked service issue, do not keep retrying |
|
|
84
84
|
|
|
85
85
|
## Browser Cleanup
|
|
86
86
|
|
|
87
|
-
Close the browser when done with `
|
|
87
|
+
Close the browser when done with `stably-browser close`.
|
|
88
88
|
|
|
89
89
|
## Wait Patterns
|
|
90
90
|
|
|
@@ -206,11 +206,11 @@ When you need to type or use environment variable values during browser interact
|
|
|
206
206
|
|
|
207
207
|
```bash
|
|
208
208
|
# CORRECT — single quotes preserve the literal ${process.env.X} for substitution
|
|
209
|
-
|
|
210
|
-
|
|
209
|
+
stably-browser fill e3 '${process.env.ACCOUNT_PASSWORD}'
|
|
210
|
+
stably-browser goto 'https://${process.env.BASE_URL}/dashboard'
|
|
211
211
|
|
|
212
212
|
# WRONG — double quotes cause bash to expand ${process.env.X} as empty
|
|
213
|
-
|
|
213
|
+
stably-browser fill e3 "${process.env.ACCOUNT_PASSWORD}"
|
|
214
214
|
```
|
|
215
215
|
|
|
216
216
|
## Browser Interaction Strategy (Priority Order)
|
|
@@ -223,13 +223,13 @@ Don't prematurely optimize with custom JS. Slower but reliable `agent.act()` is
|
|
|
223
223
|
|
|
224
224
|
## Page State Snapshots
|
|
225
225
|
|
|
226
|
-
Every `
|
|
226
|
+
Every `stably-browser` command that changes page state (`open`, `goto`, `click`, `fill`, `run-test`, etc.) returns a snapshot file path in its output. **Read that snapshot file directly — do not call `stably-browser snapshot` again.** Refs (e1, e2, etc.) are only valid from the most recent snapshot. After any command that returns a new snapshot, previous refs are stale.
|
|
227
227
|
|
|
228
228
|
**Efficient workflow:**
|
|
229
|
-
1. Run a command (e.g., `
|
|
229
|
+
1. Run a command (e.g., `stably-browser open https://example.com`)
|
|
230
230
|
2. Read the snapshot `.yml` file from the command output
|
|
231
231
|
3. Use the refs from that file immediately for the next interaction
|
|
232
232
|
|
|
233
|
-
Only use `
|
|
233
|
+
Only use `stably-browser snapshot` when you need to re-inspect the page without performing any action.
|
|
234
234
|
|
|
235
|
-
For large pages, the snapshot `.yml` file may be too large for the Read tool. Use `Grep` to search within the `.
|
|
235
|
+
For large pages, the snapshot `.yml` file may be too large for the Read tool. Use `Grep` to search within the `.stably-browser/` directory for specific elements.
|
|
@@ -73,7 +73,7 @@ Keep the user informed:
|
|
|
73
73
|
|
|
74
74
|
### Between Tests (Browser Reset)
|
|
75
75
|
|
|
76
|
-
**CRITICAL:** When verifying independent tests, run `
|
|
76
|
+
**CRITICAL:** When verifying independent tests, run `stably-browser close` after each `run-test` call before starting the next test. Browser state (cookies, auth, localStorage) persists between runs, causing cascading failures if not reset. **`stably-browser close` must be its own separate Bash call** — do NOT chain it with other commands (e.g., `close; run-test`). The browser is only fully terminated after the Bash command finishes.
|
|
77
77
|
|
|
78
78
|
### Between Batches - Check Your System Prompt
|
|
79
79
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: debugging-test-failures
|
|
3
|
-
description: Guide for debugging test execution issues including assertion failures,
|
|
3
|
+
description: Guide for debugging test execution issues including assertion failures, stably-browser errors, test setup problems, browser errors, and flaky tests. Use when tests fail, stably-browser fails, browser setup fails, or tests are flaky. Triggers on test failure, debug, stably-browser error, test won't run, setup failed, CDP error, flaky test, timeout.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Debugging Test Failures
|
|
@@ -55,9 +55,9 @@ Five types of issues cause test failures:
|
|
|
55
55
|
|
|
56
56
|
**In `fix` mode**: Delegate browser-based exploration to the `debug-worker` subagent.
|
|
57
57
|
|
|
58
|
-
**In `single`, `build`, `chat` modes**: Run `
|
|
58
|
+
**In `single`, `build`, `chat` modes**: Run `stably-browser run-test` directly (no debug subagent available). Browser persists after test runs for inspection via `stably-browser snapshot`.
|
|
59
59
|
|
|
60
|
-
**IMPORTANT:** When verifying multiple independent tests, run `
|
|
60
|
+
**IMPORTANT:** When verifying multiple independent tests, run `stably-browser close` between each `run-test` call. Stale browser state (cookies, auth sessions, localStorage) from one test will contaminate the next, causing cascading failures — especially at login. **`stably-browser close` must be its own separate Bash call** — do NOT chain it with other commands (e.g., `close; run-test`). The browser is only fully terminated after the Bash command finishes.
|
|
61
61
|
|
|
62
62
|
### When to Use Subagents
|
|
63
63
|
|
|
@@ -74,7 +74,7 @@ Five types of issues cause test failures:
|
|
|
74
74
|
|
|
75
75
|
## Debugging Workflow
|
|
76
76
|
|
|
77
|
-
1. **Run the test** and read the error output from `
|
|
77
|
+
1. **Run the test** and read the error output from `stably-browser run-test`
|
|
78
78
|
2. **Read `error-context.md`**: On failure, check `test-results/<test-name>/error-context.md`. This is the page accessibility snapshot at the moment of failure — it shows every visible element, their roles, names, and states. This tells you what the page *actually looked like* when things went wrong.
|
|
79
79
|
3. **Classify the failure by symptom:**
|
|
80
80
|
|
|
@@ -94,7 +94,7 @@ Five types of issues cause test failures:
|
|
|
94
94
|
|
|
95
95
|
## When to Open the Browser for Manual Exploration
|
|
96
96
|
|
|
97
|
-
**Open the browser** (`
|
|
97
|
+
**Open the browser** (`stably-browser open` or use the persisted session after `run-test`) when:
|
|
98
98
|
- The error-context doesn't explain the failure — you can see the page state but not *why* the selector doesn't match
|
|
99
99
|
- You need to discover interactive behavior (dropdowns, modals, hover states, animations) that a static snapshot can't capture
|
|
100
100
|
- You're writing or rewriting a test for a flow you haven't seen before
|
|
@@ -117,17 +117,17 @@ Five types of issues cause test failures:
|
|
|
117
117
|
|
|
118
118
|
### Use `run-code` to Debug Locators Interactively
|
|
119
119
|
|
|
120
|
-
**If a locator fails on the first `run-test`, don't immediately retry `run-test`.** Open the browser and test the locator interactively with `
|
|
120
|
+
**If a locator fails on the first `run-test`, don't immediately retry `run-test`.** Open the browser and test the locator interactively with `stably-browser run-code` — it gives results in seconds instead of waiting for a full test run (~2 minutes).
|
|
121
121
|
|
|
122
122
|
```bash
|
|
123
123
|
# Test a locator and see what it actually returns
|
|
124
|
-
|
|
124
|
+
stably-browser run-code "async page => {
|
|
125
125
|
const el = page.getByRole('button', { name: 'Submit' });
|
|
126
126
|
return { count: await el.count(), text: await el.first().textContent() };
|
|
127
127
|
}"
|
|
128
128
|
|
|
129
129
|
# Explore DOM structure around an element
|
|
130
|
-
|
|
130
|
+
stably-browser run-code "async page => {
|
|
131
131
|
let el = page.getByText('Filters', { exact: true });
|
|
132
132
|
for (let i = 1; i <= 5; i++) {
|
|
133
133
|
el = el.locator('..');
|
|
@@ -162,12 +162,12 @@ If you encounter `browserType.connectOverCDP` errors like "Target page has been
|
|
|
162
162
|
This is a stale browser session issue, NOT a test or config problem.
|
|
163
163
|
|
|
164
164
|
**Fix:**
|
|
165
|
-
1. Run `
|
|
166
|
-
2. Retry your operation — `
|
|
165
|
+
1. Run `stably-browser close` to stop the current browser session
|
|
166
|
+
2. Retry your operation — `stably-browser open` will start a fresh browser
|
|
167
167
|
|
|
168
168
|
## Empty run-test Output
|
|
169
169
|
|
|
170
|
-
If `
|
|
170
|
+
If `stably-browser run-test` returns empty/no output (no pass, no fail, no error):
|
|
171
171
|
|
|
172
172
|
This is NOT a tool malfunction. Empty output means zero tests matched the project filters.
|
|
173
173
|
|