playwright 1.56.0-alpha-2025-09-11 → 1.56.0-alpha-2025-09-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.
- package/README.md +2 -2
- package/lib/agents/fixer.md +8 -8
- package/lib/agents/generator.md +6 -8
- package/lib/agents/planner.md +6 -6
- package/lib/index.js +1 -0
- package/lib/mcp/browser/browserContextFactory.js +32 -25
- package/lib/mcp/browser/processUtils.js +102 -0
- package/lib/mcp/browser/watchdog.js +42 -0
- package/lib/mcp/program.js +2 -16
- package/lib/mcp/sdk/mdb.js +23 -30
- package/lib/mcp/sdk/proxyBackend.js +10 -3
- package/lib/mcp/sdk/server.js +16 -32
- package/lib/mcp/test/browserBackend.js +32 -10
- package/lib/mcp/test/testBackend.js +22 -3
- package/lib/mcp/test/testContext.js +4 -2
- package/lib/mcp/test/testTools.js +37 -13
- package/lib/program.js +3 -2
- package/lib/runner/dispatcher.js +8 -1
- package/lib/runner/failureTracker.js +4 -0
- package/lib/runner/testRunner.js +3 -1
- package/lib/runner/workerHost.js +8 -7
- package/lib/worker/testInfo.js +3 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# 🎭 Playwright
|
|
2
2
|
|
|
3
|
-
[](https://www.npmjs.com/package/playwright) <!-- GEN:chromium-version-badge -->[](https://www.npmjs.com/package/playwright) <!-- GEN:chromium-version-badge -->[](https://www.chromium.org/Home)<!-- GEN:stop --> <!-- GEN:firefox-version-badge -->[](https://www.mozilla.org/en-US/firefox/new/)<!-- GEN:stop --> <!-- GEN:webkit-version-badge -->[](https://webkit.org/)<!-- GEN:stop --> [](https://aka.ms/playwright/discord)
|
|
4
4
|
|
|
5
5
|
## [Documentation](https://playwright.dev) | [API reference](https://playwright.dev/docs/api/class-playwright)
|
|
6
6
|
|
|
@@ -8,7 +8,7 @@ Playwright is a framework for Web Testing and Automation. It allows testing [Chr
|
|
|
8
8
|
|
|
9
9
|
| | Linux | macOS | Windows |
|
|
10
10
|
| :--- | :---: | :---: | :---: |
|
|
11
|
-
| Chromium <!-- GEN:chromium-version -->141.0.7390.
|
|
11
|
+
| Chromium <!-- GEN:chromium-version -->141.0.7390.16<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
|
|
12
12
|
| WebKit <!-- GEN:webkit-version -->26.0<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
|
|
13
13
|
| Firefox <!-- GEN:firefox-version -->142.0.1<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
|
|
14
14
|
|
package/lib/agents/fixer.md
CHANGED
|
@@ -9,18 +9,18 @@ tools:
|
|
|
9
9
|
- read
|
|
10
10
|
- write
|
|
11
11
|
- edit
|
|
12
|
-
- playwright
|
|
13
|
-
- playwright
|
|
14
|
-
- playwright
|
|
15
|
-
- playwright
|
|
16
|
-
- playwright
|
|
17
|
-
- playwright
|
|
12
|
+
- playwright/browser_evaluate
|
|
13
|
+
- playwright/browser_generate_locator
|
|
14
|
+
- playwright/browser_snapshot
|
|
15
|
+
- playwright/test_debug
|
|
16
|
+
- playwright/test_list
|
|
17
|
+
- playwright/test_run
|
|
18
|
+
- playwright/test_setup_page
|
|
18
19
|
mcp-servers:
|
|
19
|
-
playwright
|
|
20
|
+
playwright:
|
|
20
21
|
type: 'local'
|
|
21
22
|
command: 'npx'
|
|
22
23
|
args: ['playwright', 'run-test-mcp-server']
|
|
23
|
-
tools: ['*']
|
|
24
24
|
---
|
|
25
25
|
|
|
26
26
|
You are the Playwright Test Fixer, an expert test automation engineer specializing in debugging and
|
package/lib/agents/generator.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: playwright-test-generator
|
|
3
3
|
description: Use this agent when you need to create automated browser tests using Playwright
|
|
4
|
-
color: blue
|
|
5
4
|
model: sonnet
|
|
5
|
+
color: blue
|
|
6
6
|
tools:
|
|
7
7
|
- ls
|
|
8
8
|
- grep
|
|
@@ -24,16 +24,12 @@ tools:
|
|
|
24
24
|
- playwright/browser_verify_text_visible
|
|
25
25
|
- playwright/browser_verify_value
|
|
26
26
|
- playwright/browser_wait_for
|
|
27
|
+
- playwright/test_setup_page
|
|
27
28
|
mcp-servers:
|
|
28
29
|
playwright:
|
|
29
30
|
type: 'local'
|
|
30
31
|
command: 'npx'
|
|
31
|
-
args:
|
|
32
|
-
- 'playwright'
|
|
33
|
-
- 'run-mcp-server'
|
|
34
|
-
- '--isolated'
|
|
35
|
-
- '--viewport-size=1280,720'
|
|
36
|
-
- '--caps=testing'
|
|
32
|
+
args: ['playwright', 'run-test-mcp-server']
|
|
37
33
|
---
|
|
38
34
|
|
|
39
35
|
You are a Playwright Test Generator, an expert in browser automation and end-to-end testing. Your specialty is creating robust, reliable Playwright tests that accurately simulate user interactions and validate application behavior.
|
|
@@ -42,7 +38,9 @@ Your process is methodical and thorough:
|
|
|
42
38
|
|
|
43
39
|
1. **Scenario Analysis**: Carefully analyze the test scenario provided, identifying all user actions, expected outcomes, and validation points. Break down complex flows into discrete, testable steps.
|
|
44
40
|
|
|
45
|
-
2. **Interactive Execution**:
|
|
41
|
+
2. **Interactive Execution**:
|
|
42
|
+
- For each test, start with the `test_setup_page` tool to set up page for the scenario
|
|
43
|
+
- Use Playwright tools to manually execute each step of the scenario in real-time
|
|
46
44
|
- Verify that each action works as expected
|
|
47
45
|
- Identify the correct locators and interaction patterns
|
|
48
46
|
- Observe actual application behavior and responses
|
package/lib/agents/planner.md
CHANGED
|
@@ -25,22 +25,22 @@ tools:
|
|
|
25
25
|
- playwright/browser_take_screenshot
|
|
26
26
|
- playwright/browser_type
|
|
27
27
|
- playwright/browser_wait_for
|
|
28
|
+
- playwright/test_setup_page
|
|
28
29
|
mcp-servers:
|
|
29
30
|
playwright:
|
|
30
31
|
type: 'local'
|
|
31
32
|
command: 'npx'
|
|
32
|
-
args:
|
|
33
|
-
- 'playwright'
|
|
34
|
-
- 'run-mcp-server'
|
|
35
|
-
- '--isolated'
|
|
36
|
-
- '--viewport-size=1280,720'
|
|
33
|
+
args: ['playwright', 'run-test-mcp-server']
|
|
37
34
|
---
|
|
38
35
|
|
|
39
36
|
You are an expert web test planner with extensive experience in quality assurance, user experience testing, and test scenario design. Your expertise includes functional testing, usability testing, edge case identification, and comprehensive test coverage planning.
|
|
40
37
|
|
|
41
38
|
When given a target web page or application, you will:
|
|
42
39
|
|
|
43
|
-
1. **Navigate and Explore**:
|
|
40
|
+
1. **Navigate and Explore**:
|
|
41
|
+
- Invoke the `test_setup_page` tool once to set up page before using any other tools
|
|
42
|
+
- Explore the aria snapshot, use browser_* tools to navigate and discover interface.
|
|
43
|
+
- Thoroughly explore the interface, identifying all interactive elements, forms, navigation paths, and functionality
|
|
44
44
|
|
|
45
45
|
2. **Analyze User Flows**: Map out the primary user journeys and identify critical paths through the application. Consider different user types and their typical behaviors.
|
|
46
46
|
|
package/lib/index.js
CHANGED
|
@@ -38,6 +38,7 @@ var import_path = __toESM(require("path"));
|
|
|
38
38
|
var playwright = __toESM(require("playwright-core"));
|
|
39
39
|
var import_registry = require("playwright-core/lib/server/registry/index");
|
|
40
40
|
var import_server = require("playwright-core/lib/server");
|
|
41
|
+
var import_processUtils = require("./processUtils");
|
|
41
42
|
var import_log = require("../log");
|
|
42
43
|
var import_config = require("./config");
|
|
43
44
|
function contextFactory(config) {
|
|
@@ -160,32 +161,32 @@ class PersistentContextFactory {
|
|
|
160
161
|
(0, import_log.testDebug)("lock user data dir", userDataDir);
|
|
161
162
|
const browserType = playwright[this.config.browser.browserName];
|
|
162
163
|
for (let i = 0; i < 5; i++) {
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
164
|
+
if (!await alreadyRunning(this.config, browserType, userDataDir))
|
|
165
|
+
break;
|
|
166
|
+
await new Promise((resolve) => setTimeout(resolve, 1e3));
|
|
167
|
+
}
|
|
168
|
+
const launchOptions = {
|
|
169
|
+
tracesDir,
|
|
170
|
+
...this.config.browser.launchOptions,
|
|
171
|
+
...this.config.browser.contextOptions,
|
|
172
|
+
handleSIGINT: false,
|
|
173
|
+
handleSIGTERM: false,
|
|
174
|
+
ignoreDefaultArgs: [
|
|
175
|
+
"--disable-extensions"
|
|
176
|
+
],
|
|
177
|
+
assistantMode: true
|
|
178
|
+
};
|
|
179
|
+
try {
|
|
180
|
+
const browserContext = await browserType.launchPersistentContext(userDataDir, launchOptions);
|
|
181
|
+
const close = () => this._closeBrowserContext(browserContext, userDataDir);
|
|
182
|
+
return { browserContext, close };
|
|
183
|
+
} catch (error) {
|
|
184
|
+
if (error.message.includes("Executable doesn't exist"))
|
|
185
|
+
throw new Error(`Browser specified in your config is not installed. Either install it (likely) or change the config.`);
|
|
186
|
+
if (error.message.includes("ProcessSingleton") || error.message.includes("Invalid URL"))
|
|
187
|
+
throw new Error(`Browser is already in use for ${userDataDir}, use --isolated to run multiple instances of the same browser`);
|
|
188
|
+
throw error;
|
|
187
189
|
}
|
|
188
|
-
throw new Error(`Browser is already in use for ${userDataDir}, use --isolated to run multiple instances of the same browser`);
|
|
189
190
|
}
|
|
190
191
|
async _closeBrowserContext(browserContext, userDataDir) {
|
|
191
192
|
(0, import_log.testDebug)("close browser context (persistent)");
|
|
@@ -204,6 +205,12 @@ class PersistentContextFactory {
|
|
|
204
205
|
return result;
|
|
205
206
|
}
|
|
206
207
|
}
|
|
208
|
+
async function alreadyRunning(config, browserType, userDataDir) {
|
|
209
|
+
const execPath = config.browser.launchOptions.executablePath ?? (0, import_processUtils.getBrowserExecPath)(config.browser.launchOptions.channel ?? browserType.name());
|
|
210
|
+
if (!execPath)
|
|
211
|
+
return false;
|
|
212
|
+
return !!(0, import_processUtils.findBrowserProcess)(execPath, userDataDir);
|
|
213
|
+
}
|
|
207
214
|
async function injectCdpPort(browserConfig) {
|
|
208
215
|
if (browserConfig.browserName === "chromium")
|
|
209
216
|
browserConfig.launchOptions.cdpPort = await findFreePort();
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
var processUtils_exports = {};
|
|
30
|
+
__export(processUtils_exports, {
|
|
31
|
+
findBrowserProcess: () => findBrowserProcess,
|
|
32
|
+
getBrowserExecPath: () => getBrowserExecPath
|
|
33
|
+
});
|
|
34
|
+
module.exports = __toCommonJS(processUtils_exports);
|
|
35
|
+
var import_child_process = __toESM(require("child_process"));
|
|
36
|
+
var import_fs = __toESM(require("fs"));
|
|
37
|
+
var import_registry = require("playwright-core/lib/server/registry/index");
|
|
38
|
+
function getBrowserExecPath(channelOrName) {
|
|
39
|
+
return import_registry.registry.findExecutable(channelOrName)?.executablePath("javascript");
|
|
40
|
+
}
|
|
41
|
+
function findBrowserProcess(execPath, arg) {
|
|
42
|
+
const predicate = (line) => line.includes(execPath) && line.includes(arg) && !line.includes("--type");
|
|
43
|
+
try {
|
|
44
|
+
switch (process.platform) {
|
|
45
|
+
case "darwin":
|
|
46
|
+
return findProcessMacos(predicate);
|
|
47
|
+
case "linux":
|
|
48
|
+
return findProcessLinux(predicate);
|
|
49
|
+
case "win32":
|
|
50
|
+
return findProcessWindows(execPath, arg, predicate);
|
|
51
|
+
default:
|
|
52
|
+
return void 0;
|
|
53
|
+
}
|
|
54
|
+
} catch {
|
|
55
|
+
return void 0;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
function findProcessLinux(predicate) {
|
|
59
|
+
const procDirs = import_fs.default.readdirSync("/proc").filter((name) => /^\d+$/.test(name));
|
|
60
|
+
for (const pid of procDirs) {
|
|
61
|
+
try {
|
|
62
|
+
const cmdlineBuffer = import_fs.default.readFileSync(`/proc/${pid}/cmdline`);
|
|
63
|
+
const cmdline = cmdlineBuffer.toString().replace(/\0/g, " ").trim();
|
|
64
|
+
if (predicate(cmdline))
|
|
65
|
+
return `${pid} ${cmdline}`;
|
|
66
|
+
} catch {
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return void 0;
|
|
71
|
+
}
|
|
72
|
+
function findProcessMacos(predicate) {
|
|
73
|
+
const result = import_child_process.default.spawnSync("/bin/ps", ["-axo", "pid=,command="]);
|
|
74
|
+
if (result.status !== 0 || !result.stdout)
|
|
75
|
+
return void 0;
|
|
76
|
+
return findMatchingLine(result.stdout.toString(), predicate);
|
|
77
|
+
}
|
|
78
|
+
function findProcessWindows(execPath, arg, predicate) {
|
|
79
|
+
const psEscape = (path) => `'${path.replaceAll("'", "''")}'`;
|
|
80
|
+
const filter = `$_.ExecutablePath -eq ${psEscape(execPath)} -and $_.CommandLine.Contains(${psEscape(arg)}) -and $_.CommandLine -notmatch '--type'`;
|
|
81
|
+
const ps = import_child_process.default.spawnSync(
|
|
82
|
+
"powershell.exe",
|
|
83
|
+
[
|
|
84
|
+
"-NoProfile",
|
|
85
|
+
"-Command",
|
|
86
|
+
`Get-CimInstance Win32_Process | Where-Object { ${filter} } | Select-Object -Property ProcessId,CommandLine | ForEach-Object { "$($_.ProcessId) $($_.CommandLine)" }`
|
|
87
|
+
],
|
|
88
|
+
{ encoding: "utf8" }
|
|
89
|
+
);
|
|
90
|
+
if (ps.status !== 0 || !ps.stdout)
|
|
91
|
+
return void 0;
|
|
92
|
+
return findMatchingLine(ps.stdout.toString(), predicate);
|
|
93
|
+
}
|
|
94
|
+
function findMatchingLine(psOutput, predicate) {
|
|
95
|
+
const lines = psOutput.split("\n").map((l) => l.trim()).filter(Boolean);
|
|
96
|
+
return lines.find(predicate);
|
|
97
|
+
}
|
|
98
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
99
|
+
0 && (module.exports = {
|
|
100
|
+
findBrowserProcess,
|
|
101
|
+
getBrowserExecPath
|
|
102
|
+
});
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
var watchdog_exports = {};
|
|
20
|
+
__export(watchdog_exports, {
|
|
21
|
+
setupExitWatchdog: () => setupExitWatchdog
|
|
22
|
+
});
|
|
23
|
+
module.exports = __toCommonJS(watchdog_exports);
|
|
24
|
+
var import_context = require("./context");
|
|
25
|
+
function setupExitWatchdog() {
|
|
26
|
+
let isExiting = false;
|
|
27
|
+
const handleExit = async () => {
|
|
28
|
+
if (isExiting)
|
|
29
|
+
return;
|
|
30
|
+
isExiting = true;
|
|
31
|
+
setTimeout(() => process.exit(0), 15e3);
|
|
32
|
+
await import_context.Context.disposeAll();
|
|
33
|
+
process.exit(0);
|
|
34
|
+
};
|
|
35
|
+
process.stdin.on("close", handleExit);
|
|
36
|
+
process.on("SIGINT", handleExit);
|
|
37
|
+
process.on("SIGTERM", handleExit);
|
|
38
|
+
}
|
|
39
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
40
|
+
0 && (module.exports = {
|
|
41
|
+
setupExitWatchdog
|
|
42
|
+
});
|
package/lib/mcp/program.js
CHANGED
|
@@ -34,7 +34,7 @@ module.exports = __toCommonJS(program_exports);
|
|
|
34
34
|
var import_utilsBundle = require("playwright-core/lib/utilsBundle");
|
|
35
35
|
var mcpServer = __toESM(require("./sdk/server"));
|
|
36
36
|
var import_config = require("./browser/config");
|
|
37
|
-
var
|
|
37
|
+
var import_watchdog = require("./browser/watchdog");
|
|
38
38
|
var import_browserContextFactory = require("./browser/browserContextFactory");
|
|
39
39
|
var import_proxyBackend = require("./sdk/proxyBackend");
|
|
40
40
|
var import_browserServerBackend = require("./browser/browserServerBackend");
|
|
@@ -42,7 +42,7 @@ var import_extensionContextFactory = require("./extension/extensionContextFactor
|
|
|
42
42
|
var import_host = require("./vscode/host");
|
|
43
43
|
function decorateCommand(command, version) {
|
|
44
44
|
command.option("--allowed-origins <origins>", "semicolon-separated list of origins to allow the browser to request. Default is to allow all.", import_config.semicolonSeparatedList).option("--blocked-origins <origins>", "semicolon-separated list of origins to block the browser from requesting. Blocklist is evaluated before allowlist. If used without the allowlist, requests not matching the blocklist are still allowed.", import_config.semicolonSeparatedList).option("--block-service-workers", "block service workers").option("--browser <browser>", "browser or chrome channel to use, possible values: chrome, firefox, webkit, msedge.").option("--caps <caps>", "comma-separated list of additional capabilities to enable, possible values: vision, pdf.", import_config.commaSeparatedList).option("--cdp-endpoint <endpoint>", "CDP endpoint to connect to.").option("--cdp-header <headers...>", "CDP headers to send with the connect request, multiple can be specified.", import_config.headerParser).option("--config <path>", "path to the configuration file.").option("--device <device>", 'device to emulate, for example: "iPhone 15"').option("--executable-path <path>", "path to the browser executable.").option("--extension", 'Connect to a running browser instance (Edge/Chrome only). Requires the "Playwright MCP Bridge" browser extension to be installed.').option("--headless", "run browser in headless mode, headed by default").option("--host <host>", "host to bind server to. Default is localhost. Use 0.0.0.0 to bind to all interfaces.").option("--ignore-https-errors", "ignore https errors").option("--isolated", "keep the browser profile in memory, do not save it to disk.").option("--image-responses <mode>", 'whether to send image responses to the client. Can be "allow" or "omit", Defaults to "allow".').option("--no-sandbox", "disable the sandbox for all process types that are normally sandboxed.").option("--output-dir <path>", "path to the directory for output files.").option("--port <port>", "port to listen on for SSE transport.").option("--proxy-bypass <bypass>", 'comma-separated domains to bypass proxy, for example ".com,chromium.org,.domain.com"').option("--proxy-server <proxy>", 'specify proxy server, for example "http://myproxy:3128" or "socks5://myproxy:8080"').option("--save-session", "Whether to save the Playwright MCP session into the output directory.").option("--save-trace", "Whether to save the Playwright Trace of the session into the output directory.").option("--secrets <path>", "path to a file containing secrets in the dotenv format", import_config.dotenvFileLoader).option("--storage-state <path>", "path to the storage state file for isolated sessions.").option("--timeout-action <timeout>", "specify action timeout in milliseconds, defaults to 5000ms", import_config.numberParser).option("--timeout-navigation <timeout>", "specify navigation timeout in milliseconds, defaults to 60000ms", import_config.numberParser).option("--user-agent <ua string>", "specify user agent string").option("--user-data-dir <path>", "path to the user data directory. If not specified, a temporary directory will be created.").option("--viewport-size <size>", 'specify browser viewport size in pixels, for example "1280, 720"').addOption(new import_utilsBundle.ProgramOption("--connect-tool", "Allow to switch between different browser connection methods.").hideHelp()).addOption(new import_utilsBundle.ProgramOption("--vscode", "VS Code tools.").hideHelp()).addOption(new import_utilsBundle.ProgramOption("--vision", "Legacy option, use --caps=vision instead").hideHelp()).action(async (options) => {
|
|
45
|
-
setupExitWatchdog();
|
|
45
|
+
(0, import_watchdog.setupExitWatchdog)();
|
|
46
46
|
if (options.vision) {
|
|
47
47
|
console.error("The --vision option is deprecated, use --caps=vision instead");
|
|
48
48
|
options.caps = "vision";
|
|
@@ -95,20 +95,6 @@ function decorateCommand(command, version) {
|
|
|
95
95
|
await mcpServer.start(factory, config.server);
|
|
96
96
|
});
|
|
97
97
|
}
|
|
98
|
-
function setupExitWatchdog() {
|
|
99
|
-
let isExiting = false;
|
|
100
|
-
const handleExit = async () => {
|
|
101
|
-
if (isExiting)
|
|
102
|
-
return;
|
|
103
|
-
isExiting = true;
|
|
104
|
-
setTimeout(() => process.exit(0), 15e3);
|
|
105
|
-
await import_context.Context.disposeAll();
|
|
106
|
-
process.exit(0);
|
|
107
|
-
};
|
|
108
|
-
process.stdin.on("close", handleExit);
|
|
109
|
-
process.on("SIGINT", handleExit);
|
|
110
|
-
process.on("SIGTERM", handleExit);
|
|
111
|
-
}
|
|
112
98
|
// Annotate the CommonJS export names for ESM import in node:
|
|
113
99
|
0 && (module.exports = {
|
|
114
100
|
decorateCommand
|
package/lib/mcp/sdk/mdb.js
CHANGED
|
@@ -46,21 +46,19 @@ const z = mcpBundle.z;
|
|
|
46
46
|
class MDBBackend {
|
|
47
47
|
constructor(topLevelBackend) {
|
|
48
48
|
this._stack = [];
|
|
49
|
-
this._initialized = false;
|
|
50
49
|
this._topLevelBackend = topLevelBackend;
|
|
51
50
|
}
|
|
52
|
-
async initialize(server) {
|
|
53
|
-
if (this.
|
|
54
|
-
|
|
55
|
-
this._initialized = true;
|
|
56
|
-
const transport = await (0, import_server.wrapInProcess)(this._topLevelBackend);
|
|
57
|
-
await this._pushClient(transport);
|
|
51
|
+
async initialize(server, clientVersion, roots) {
|
|
52
|
+
if (!this._roots)
|
|
53
|
+
this._roots = roots;
|
|
58
54
|
}
|
|
59
55
|
async listTools() {
|
|
60
|
-
const
|
|
56
|
+
const client = await this._client();
|
|
57
|
+
const response = await client.listTools();
|
|
61
58
|
return response.tools;
|
|
62
59
|
}
|
|
63
60
|
async callTool(name, args) {
|
|
61
|
+
await this._client();
|
|
64
62
|
if (name === pushToolsSchema.name)
|
|
65
63
|
return await this._pushTools(pushToolsSchema.inputSchema.parse(args || {}));
|
|
66
64
|
const interruptPromise = new import_utils.ManualPromise();
|
|
@@ -69,26 +67,20 @@ class MDBBackend {
|
|
|
69
67
|
while (entry && !entry.toolNames.includes(name)) {
|
|
70
68
|
mdbDebug("popping client from stack for ", name);
|
|
71
69
|
this._stack.shift();
|
|
72
|
-
await entry.client.close();
|
|
70
|
+
await entry.client.close().catch(errorsDebug);
|
|
73
71
|
entry = this._stack[0];
|
|
74
72
|
}
|
|
75
73
|
if (!entry)
|
|
76
74
|
throw new Error(`Tool ${name} not found in the tool stack`);
|
|
75
|
+
const client = await this._client();
|
|
77
76
|
const resultPromise = new import_utils.ManualPromise();
|
|
78
77
|
entry.resultPromise = resultPromise;
|
|
79
|
-
|
|
78
|
+
client.callTool({
|
|
80
79
|
name,
|
|
81
80
|
arguments: args
|
|
82
81
|
}).then((result2) => {
|
|
83
82
|
resultPromise.resolve(result2);
|
|
84
|
-
}).catch((e) =>
|
|
85
|
-
mdbDebug("error in client call", e);
|
|
86
|
-
if (this._stack.length < 2)
|
|
87
|
-
throw e;
|
|
88
|
-
this._stack.shift();
|
|
89
|
-
const prevEntry = this._stack[0];
|
|
90
|
-
void prevEntry.resultPromise.then((result2) => resultPromise.resolve(result2));
|
|
91
|
-
});
|
|
83
|
+
}).catch((e) => resultPromise.reject(e));
|
|
92
84
|
const result = await Promise.race([interruptPromise, resultPromise]);
|
|
93
85
|
if (interruptPromise.isDone())
|
|
94
86
|
mdbDebug("client call intercepted", result);
|
|
@@ -96,11 +88,12 @@ class MDBBackend {
|
|
|
96
88
|
mdbDebug("client call result", result);
|
|
97
89
|
return result;
|
|
98
90
|
}
|
|
99
|
-
_client() {
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
91
|
+
async _client() {
|
|
92
|
+
if (!this._stack.length) {
|
|
93
|
+
const transport = await (0, import_server.wrapInProcess)(this._topLevelBackend);
|
|
94
|
+
await this._pushClient(transport);
|
|
95
|
+
}
|
|
96
|
+
return this._stack[0].client;
|
|
104
97
|
}
|
|
105
98
|
async _pushTools(params) {
|
|
106
99
|
mdbDebug("pushing tools to the stack", params.mcpUrl);
|
|
@@ -110,7 +103,8 @@ class MDBBackend {
|
|
|
110
103
|
}
|
|
111
104
|
async _pushClient(transport, introMessage) {
|
|
112
105
|
mdbDebug("pushing client to the stack");
|
|
113
|
-
const client = new mcpBundle.Client({ name: "Internal client", version: "0.0.0" });
|
|
106
|
+
const client = new mcpBundle.Client({ name: "Internal client", version: "0.0.0" }, { capabilities: { roots: {} } });
|
|
107
|
+
client.setRequestHandler(mcpBundle.ListRootsRequestSchema, () => ({ roots: this._roots || [] }));
|
|
114
108
|
client.setRequestHandler(mcpBundle.PingRequestSchema, () => ({}));
|
|
115
109
|
await client.connect(transport);
|
|
116
110
|
mdbDebug("connected to the new client");
|
|
@@ -151,7 +145,7 @@ async function runMainBackend(backendFactory, options) {
|
|
|
151
145
|
await mcpServer.connect(factory, new mcpBundle.StdioServerTransport(), false);
|
|
152
146
|
}
|
|
153
147
|
async function runOnPauseBackendLoop(backend, introMessage) {
|
|
154
|
-
const wrappedBackend = new
|
|
148
|
+
const wrappedBackend = new ServerBackendWithCloseListener(backend);
|
|
155
149
|
const factory = {
|
|
156
150
|
name: "on-pause-backend",
|
|
157
151
|
nameInConfig: "on-pause-backend",
|
|
@@ -184,11 +178,10 @@ async function startAsHttp(backendFactory, options) {
|
|
|
184
178
|
await mcpHttp.installHttpTransport(httpServer, backendFactory);
|
|
185
179
|
return mcpHttp.httpAddressToString(httpServer.address());
|
|
186
180
|
}
|
|
187
|
-
class
|
|
181
|
+
class ServerBackendWithCloseListener {
|
|
188
182
|
constructor(backend) {
|
|
189
|
-
this.
|
|
183
|
+
this._serverClosedPromise = new import_utils.ManualPromise();
|
|
190
184
|
this._backend = backend;
|
|
191
|
-
this._backend.requestSelfDestruct = () => this._selfDestructPromise.resolve();
|
|
192
185
|
}
|
|
193
186
|
async initialize(server, clientVersion, roots) {
|
|
194
187
|
await this._backend.initialize?.(server, clientVersion, roots);
|
|
@@ -201,10 +194,10 @@ class OnceTimeServerBackendWrapper {
|
|
|
201
194
|
}
|
|
202
195
|
serverClosed(server) {
|
|
203
196
|
this._backend.serverClosed?.(server);
|
|
204
|
-
this.
|
|
197
|
+
this._serverClosedPromise.resolve();
|
|
205
198
|
}
|
|
206
199
|
async waitForClosed() {
|
|
207
|
-
await this.
|
|
200
|
+
await this._serverClosedPromise;
|
|
208
201
|
}
|
|
209
202
|
}
|
|
210
203
|
// Annotate the CommonJS export names for ESM import in node:
|
|
@@ -43,10 +43,10 @@ class ProxyBackend {
|
|
|
43
43
|
}
|
|
44
44
|
async initialize(server, clientVersion, roots) {
|
|
45
45
|
this._roots = roots;
|
|
46
|
-
await this._setCurrentClient(this._mcpProviders[0]);
|
|
47
46
|
}
|
|
48
47
|
async listTools() {
|
|
49
|
-
const
|
|
48
|
+
const currentClient = await this._ensureCurrentClient();
|
|
49
|
+
const response = await currentClient.listTools();
|
|
50
50
|
if (this._mcpProviders.length === 1)
|
|
51
51
|
return response.tools;
|
|
52
52
|
return [
|
|
@@ -57,7 +57,8 @@ class ProxyBackend {
|
|
|
57
57
|
async callTool(name, args) {
|
|
58
58
|
if (name === this._contextSwitchTool.name)
|
|
59
59
|
return this._callContextSwitchTool(args);
|
|
60
|
-
|
|
60
|
+
const currentClient = await this._ensureCurrentClient();
|
|
61
|
+
return await currentClient.callTool({
|
|
61
62
|
name,
|
|
62
63
|
arguments: args
|
|
63
64
|
});
|
|
@@ -100,6 +101,11 @@ Error: ${error}
|
|
|
100
101
|
}
|
|
101
102
|
};
|
|
102
103
|
}
|
|
104
|
+
async _ensureCurrentClient() {
|
|
105
|
+
if (this._currentClient)
|
|
106
|
+
return this._currentClient;
|
|
107
|
+
return await this._setCurrentClient(this._mcpProviders[0]);
|
|
108
|
+
}
|
|
103
109
|
async _setCurrentClient(factory) {
|
|
104
110
|
await this._currentClient?.close();
|
|
105
111
|
this._currentClient = void 0;
|
|
@@ -114,6 +120,7 @@ Error: ${error}
|
|
|
114
120
|
const transport = await factory.connect();
|
|
115
121
|
await client.connect(transport);
|
|
116
122
|
this._currentClient = client;
|
|
123
|
+
return client;
|
|
117
124
|
}
|
|
118
125
|
}
|
|
119
126
|
// Annotate the CommonJS export names for ESM import in node:
|
package/lib/mcp/sdk/server.js
CHANGED
|
@@ -39,7 +39,6 @@ var mcpBundle = __toESM(require("./bundle"));
|
|
|
39
39
|
var import_http = require("./http");
|
|
40
40
|
var import_inProcessTransport = require("./inProcessTransport");
|
|
41
41
|
const serverDebug = (0, import_utilsBundle.debug)("pw:mcp:server");
|
|
42
|
-
const errorsDebug = (0, import_utilsBundle.debug)("pw:mcp:errors");
|
|
43
42
|
async function connect(factory, transport, runHeartbeat) {
|
|
44
43
|
const server = createServer(factory.name, factory.version, factory.create(), runHeartbeat);
|
|
45
44
|
await server.connect(transport);
|
|
@@ -49,9 +48,6 @@ async function wrapInProcess(backend) {
|
|
|
49
48
|
return new import_inProcessTransport.InProcessTransport(server);
|
|
50
49
|
}
|
|
51
50
|
function createServer(name, version, backend, runHeartbeat) {
|
|
52
|
-
let initializedPromiseResolve = () => {
|
|
53
|
-
};
|
|
54
|
-
const initializedPromise = new Promise((resolve) => initializedPromiseResolve = resolve);
|
|
55
51
|
const server = new mcpBundle.Server({ name, version }, {
|
|
56
52
|
capabilities: {
|
|
57
53
|
tools: {}
|
|
@@ -59,19 +55,16 @@ function createServer(name, version, backend, runHeartbeat) {
|
|
|
59
55
|
});
|
|
60
56
|
server.setRequestHandler(mcpBundle.ListToolsRequestSchema, async () => {
|
|
61
57
|
serverDebug("listTools");
|
|
62
|
-
await initializedPromise;
|
|
63
58
|
const tools = await backend.listTools();
|
|
64
59
|
return { tools };
|
|
65
60
|
});
|
|
66
|
-
let
|
|
61
|
+
let initializePromise;
|
|
67
62
|
server.setRequestHandler(mcpBundle.CallToolRequestSchema, async (request) => {
|
|
68
63
|
serverDebug("callTool", request);
|
|
69
|
-
await initializedPromise;
|
|
70
|
-
if (runHeartbeat && !heartbeatRunning) {
|
|
71
|
-
heartbeatRunning = true;
|
|
72
|
-
startHeartbeat(server);
|
|
73
|
-
}
|
|
74
64
|
try {
|
|
65
|
+
if (!initializePromise)
|
|
66
|
+
initializePromise = initializeServer(server, backend, runHeartbeat);
|
|
67
|
+
await initializePromise;
|
|
75
68
|
return await backend.callTool(request.params.name, request.params.arguments || {});
|
|
76
69
|
} catch (error) {
|
|
77
70
|
return {
|
|
@@ -80,30 +73,21 @@ function createServer(name, version, backend, runHeartbeat) {
|
|
|
80
73
|
};
|
|
81
74
|
}
|
|
82
75
|
});
|
|
83
|
-
addServerListener(server, "initialized", async () => {
|
|
84
|
-
try {
|
|
85
|
-
const capabilities = server.getClientCapabilities();
|
|
86
|
-
let clientRoots = [];
|
|
87
|
-
if (capabilities?.roots) {
|
|
88
|
-
for (let i = 0; i < 2; i++) {
|
|
89
|
-
try {
|
|
90
|
-
const { roots } = await server.listRoots(void 0, { timeout: 2e3 });
|
|
91
|
-
clientRoots = roots;
|
|
92
|
-
} catch (e) {
|
|
93
|
-
continue;
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
const clientVersion = server.getClientVersion() ?? { name: "unknown", version: "unknown" };
|
|
98
|
-
await backend.initialize?.(server, clientVersion, clientRoots);
|
|
99
|
-
initializedPromiseResolve();
|
|
100
|
-
} catch (e) {
|
|
101
|
-
errorsDebug(e);
|
|
102
|
-
}
|
|
103
|
-
});
|
|
104
76
|
addServerListener(server, "close", () => backend.serverClosed?.(server));
|
|
105
77
|
return server;
|
|
106
78
|
}
|
|
79
|
+
const initializeServer = async (server, backend, runHeartbeat) => {
|
|
80
|
+
const capabilities = server.getClientCapabilities();
|
|
81
|
+
let clientRoots = [];
|
|
82
|
+
if (capabilities?.roots) {
|
|
83
|
+
const { roots } = await server.listRoots();
|
|
84
|
+
clientRoots = roots;
|
|
85
|
+
}
|
|
86
|
+
const clientVersion = server.getClientVersion() ?? { name: "unknown", version: "unknown" };
|
|
87
|
+
await backend.initialize?.(server, clientVersion, clientRoots);
|
|
88
|
+
if (runHeartbeat)
|
|
89
|
+
startHeartbeat(server);
|
|
90
|
+
};
|
|
107
91
|
const startHeartbeat = (server) => {
|
|
108
92
|
const beat = () => {
|
|
109
93
|
Promise.race([
|
|
@@ -28,6 +28,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
28
28
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
29
|
var browserBackend_exports = {};
|
|
30
30
|
__export(browserBackend_exports, {
|
|
31
|
+
runBrowserBackendAtEnd: () => runBrowserBackendAtEnd,
|
|
31
32
|
runBrowserBackendOnError: () => runBrowserBackendOnError
|
|
32
33
|
});
|
|
33
34
|
module.exports = __toCommonJS(browserBackend_exports);
|
|
@@ -40,15 +41,6 @@ async function runBrowserBackendOnError(page, message) {
|
|
|
40
41
|
const testInfo = (0, import_globals.currentTestInfo)();
|
|
41
42
|
if (!testInfo || !testInfo._pauseOnError())
|
|
42
43
|
return;
|
|
43
|
-
const browserContextFactory = {
|
|
44
|
-
createContext: async (clientInfo, abortSignal, toolName) => {
|
|
45
|
-
return {
|
|
46
|
-
browserContext: page.context(),
|
|
47
|
-
close: async () => {
|
|
48
|
-
}
|
|
49
|
-
};
|
|
50
|
-
}
|
|
51
|
-
};
|
|
52
44
|
const config = {
|
|
53
45
|
...import_config.defaultConfig,
|
|
54
46
|
capabilities: ["testing"]
|
|
@@ -62,9 +54,39 @@ ${snapshot}
|
|
|
62
54
|
|
|
63
55
|
### Task
|
|
64
56
|
Try recovering from the error prior to continuing`;
|
|
65
|
-
await mcp.runOnPauseBackendLoop(new import_browserServerBackend.BrowserServerBackend(config,
|
|
57
|
+
await mcp.runOnPauseBackendLoop(new import_browserServerBackend.BrowserServerBackend(config, identityFactory(page.context())), introMessage);
|
|
58
|
+
}
|
|
59
|
+
async function runBrowserBackendAtEnd(context) {
|
|
60
|
+
const testInfo = (0, import_globals.currentTestInfo)();
|
|
61
|
+
if (!testInfo || !testInfo._pauseAtEnd())
|
|
62
|
+
return;
|
|
63
|
+
const page = context.pages()[0];
|
|
64
|
+
if (!page)
|
|
65
|
+
return;
|
|
66
|
+
const snapshot = await page._snapshotForAI();
|
|
67
|
+
const introMessage = `### Paused at end of test. ready for interaction
|
|
68
|
+
|
|
69
|
+
### Current page snapshot:
|
|
70
|
+
${snapshot}`;
|
|
71
|
+
const config = {
|
|
72
|
+
...import_config.defaultConfig,
|
|
73
|
+
capabilities: ["testing"]
|
|
74
|
+
};
|
|
75
|
+
await mcp.runOnPauseBackendLoop(new import_browserServerBackend.BrowserServerBackend(config, identityFactory(context)), introMessage);
|
|
76
|
+
}
|
|
77
|
+
function identityFactory(browserContext) {
|
|
78
|
+
return {
|
|
79
|
+
createContext: async (clientInfo, abortSignal, toolName) => {
|
|
80
|
+
return {
|
|
81
|
+
browserContext,
|
|
82
|
+
close: async () => {
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
};
|
|
66
87
|
}
|
|
67
88
|
// Annotate the CommonJS export names for ESM import in node:
|
|
68
89
|
0 && (module.exports = {
|
|
90
|
+
runBrowserBackendAtEnd,
|
|
69
91
|
runBrowserBackendOnError
|
|
70
92
|
});
|
|
@@ -31,16 +31,35 @@ __export(testBackend_exports, {
|
|
|
31
31
|
TestServerBackend: () => TestServerBackend
|
|
32
32
|
});
|
|
33
33
|
module.exports = __toCommonJS(testBackend_exports);
|
|
34
|
+
var import_url = require("url");
|
|
34
35
|
var mcp = __toESM(require("../sdk/exports"));
|
|
35
36
|
var import_testContext = require("./testContext");
|
|
36
37
|
var import_testTools = require("./testTools.js");
|
|
37
38
|
var import_tools = require("../browser/tools");
|
|
39
|
+
var import_configLoader = require("../../common/configLoader");
|
|
38
40
|
class TestServerBackend {
|
|
39
|
-
constructor(
|
|
41
|
+
constructor(configOption, options) {
|
|
40
42
|
this.name = "Playwright";
|
|
41
43
|
this.version = "0.0.1";
|
|
42
|
-
this._tools = [import_testTools.listTests, import_testTools.runTests, import_testTools.debugTest];
|
|
43
|
-
this._context = new import_testContext.TestContext(
|
|
44
|
+
this._tools = [import_testTools.listTests, import_testTools.runTests, import_testTools.debugTest, import_testTools.setupPage];
|
|
45
|
+
this._context = new import_testContext.TestContext(options);
|
|
46
|
+
this._configOption = configOption;
|
|
47
|
+
}
|
|
48
|
+
async initialize(server, clientVersion, roots) {
|
|
49
|
+
if (this._configOption) {
|
|
50
|
+
this._context.setConfigLocation((0, import_configLoader.resolveConfigLocation)(this._configOption));
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
if (roots.length > 0) {
|
|
54
|
+
const firstRootUri = roots[0]?.uri;
|
|
55
|
+
const url = firstRootUri ? new URL(firstRootUri) : void 0;
|
|
56
|
+
const folder = url ? (0, import_url.fileURLToPath)(url) : void 0;
|
|
57
|
+
if (folder) {
|
|
58
|
+
this._context.setConfigLocation((0, import_configLoader.resolveConfigLocation)(folder));
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
throw new Error("No config option or MCP root path provided");
|
|
44
63
|
}
|
|
45
64
|
async listTools() {
|
|
46
65
|
return [
|
|
@@ -23,10 +23,12 @@ __export(testContext_exports, {
|
|
|
23
23
|
module.exports = __toCommonJS(testContext_exports);
|
|
24
24
|
var import_testRunner = require("../../runner/testRunner");
|
|
25
25
|
class TestContext {
|
|
26
|
-
constructor(
|
|
27
|
-
this.configLocation = configLocation;
|
|
26
|
+
constructor(options) {
|
|
28
27
|
this.options = options;
|
|
29
28
|
}
|
|
29
|
+
setConfigLocation(configLocation) {
|
|
30
|
+
this.configLocation = configLocation;
|
|
31
|
+
}
|
|
30
32
|
async createTestRunner() {
|
|
31
33
|
if (this._testRunner)
|
|
32
34
|
await this._testRunner.stopTests();
|
|
@@ -30,7 +30,8 @@ var testTools_exports = {};
|
|
|
30
30
|
__export(testTools_exports, {
|
|
31
31
|
debugTest: () => debugTest,
|
|
32
32
|
listTests: () => listTests,
|
|
33
|
-
runTests: () => runTests
|
|
33
|
+
runTests: () => runTests,
|
|
34
|
+
setupPage: () => setupPage
|
|
34
35
|
});
|
|
35
36
|
module.exports = __toCommonJS(testTools_exports);
|
|
36
37
|
var import_utils = require("playwright-core/lib/utils");
|
|
@@ -42,7 +43,7 @@ var import_testTool = require("./testTool");
|
|
|
42
43
|
var import_streams = require("./streams");
|
|
43
44
|
const listTests = (0, import_testTool.defineTestTool)({
|
|
44
45
|
schema: {
|
|
45
|
-
name: "
|
|
46
|
+
name: "test_list",
|
|
46
47
|
title: "List tests",
|
|
47
48
|
description: "List tests",
|
|
48
49
|
inputSchema: import_bundle.z.object({}),
|
|
@@ -60,7 +61,7 @@ const listTests = (0, import_testTool.defineTestTool)({
|
|
|
60
61
|
});
|
|
61
62
|
const runTests = (0, import_testTool.defineTestTool)({
|
|
62
63
|
schema: {
|
|
63
|
-
name: "
|
|
64
|
+
name: "test_run",
|
|
64
65
|
title: "Run tests",
|
|
65
66
|
description: "Run tests",
|
|
66
67
|
inputSchema: import_bundle.z.object({
|
|
@@ -89,7 +90,7 @@ const runTests = (0, import_testTool.defineTestTool)({
|
|
|
89
90
|
});
|
|
90
91
|
const debugTest = (0, import_testTool.defineTestTool)({
|
|
91
92
|
schema: {
|
|
92
|
-
name: "
|
|
93
|
+
name: "test_debug",
|
|
93
94
|
title: "Debug single test",
|
|
94
95
|
description: "Debug single test",
|
|
95
96
|
inputSchema: import_bundle.z.object({
|
|
@@ -101,14 +102,7 @@ const debugTest = (0, import_testTool.defineTestTool)({
|
|
|
101
102
|
type: "readOnly"
|
|
102
103
|
},
|
|
103
104
|
handle: async (context, params) => {
|
|
104
|
-
const stream =
|
|
105
|
-
const screen = {
|
|
106
|
-
...import_base.terminalScreen,
|
|
107
|
-
isTTY: false,
|
|
108
|
-
colors: import_utils.noColors,
|
|
109
|
-
stdout: stream,
|
|
110
|
-
stderr: stream
|
|
111
|
-
};
|
|
105
|
+
const { screen, stream } = createScreen();
|
|
112
106
|
const configDir = context.configLocation.configDir;
|
|
113
107
|
const reporter = new import_list.default({ configDir, screen });
|
|
114
108
|
const testRunner = await context.createTestRunner();
|
|
@@ -129,6 +123,35 @@ const debugTest = (0, import_testTool.defineTestTool)({
|
|
|
129
123
|
};
|
|
130
124
|
}
|
|
131
125
|
});
|
|
126
|
+
const setupPage = (0, import_testTool.defineTestTool)({
|
|
127
|
+
schema: {
|
|
128
|
+
name: "test_setup_page",
|
|
129
|
+
title: "Setup page",
|
|
130
|
+
description: "Runs a blank test to setup the page for interaction",
|
|
131
|
+
inputSchema: import_bundle.z.object({
|
|
132
|
+
testLocation: import_bundle.z.string().describe('Location of the blank test to use for setup. For example: "test/e2e/file.spec.ts:20"')
|
|
133
|
+
}),
|
|
134
|
+
type: "readOnly"
|
|
135
|
+
},
|
|
136
|
+
handle: async (context, params) => {
|
|
137
|
+
const { screen, stream } = createScreen();
|
|
138
|
+
const configDir = context.configLocation.configDir;
|
|
139
|
+
const reporter = new import_list.default({ configDir, screen });
|
|
140
|
+
const testRunner = await context.createTestRunner();
|
|
141
|
+
const result = await testRunner.runTests(reporter, {
|
|
142
|
+
headed: !context.options?.headless,
|
|
143
|
+
locations: [params.testLocation],
|
|
144
|
+
timeout: 0,
|
|
145
|
+
workers: 1,
|
|
146
|
+
pauseAtEnd: true
|
|
147
|
+
});
|
|
148
|
+
const text = stream.content();
|
|
149
|
+
return {
|
|
150
|
+
content: [{ type: "text", text }],
|
|
151
|
+
isError: result.status !== "passed"
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
});
|
|
132
155
|
function createScreen() {
|
|
133
156
|
const stream = new import_streams.StringWriteStream();
|
|
134
157
|
const screen = {
|
|
@@ -144,5 +167,6 @@ function createScreen() {
|
|
|
144
167
|
0 && (module.exports = {
|
|
145
168
|
debugTest,
|
|
146
169
|
listTests,
|
|
147
|
-
runTests
|
|
170
|
+
runTests,
|
|
171
|
+
setupPage
|
|
148
172
|
});
|
package/lib/program.js
CHANGED
|
@@ -49,6 +49,7 @@ var import_reporters = require("./runner/reporters");
|
|
|
49
49
|
var import_exports = require("./mcp/sdk/exports");
|
|
50
50
|
var import_testBackend = require("./mcp/test/testBackend");
|
|
51
51
|
var import_program3 = require("./mcp/program");
|
|
52
|
+
var import_watchdog = require("./mcp/browser/watchdog");
|
|
52
53
|
var import_generateAgents = require("./agents/generateAgents");
|
|
53
54
|
const packageJSON = require("../package.json");
|
|
54
55
|
function addTestCommand(program3) {
|
|
@@ -157,12 +158,12 @@ function addTestMCPServerCommand(program3) {
|
|
|
157
158
|
command.option("--host <host>", "host to bind server to. Default is localhost. Use 0.0.0.0 to bind to all interfaces.");
|
|
158
159
|
command.option("--port <port>", "port to listen on for SSE transport.");
|
|
159
160
|
command.action(async (options) => {
|
|
160
|
-
|
|
161
|
+
(0, import_watchdog.setupExitWatchdog)();
|
|
161
162
|
const backendFactory = {
|
|
162
163
|
name: "Playwright Test Runner",
|
|
163
164
|
nameInConfig: "playwright-test-runner",
|
|
164
165
|
version: packageJSON.version,
|
|
165
|
-
create: () => new import_testBackend.TestServerBackend(
|
|
166
|
+
create: () => new import_testBackend.TestServerBackend(options.config, { muteConsole: options.port === void 0, headless: options.headless })
|
|
166
167
|
};
|
|
167
168
|
const mdbUrl = await (0, import_exports.runMainBackend)(backendFactory, { port: options.port === void 0 ? void 0 : +options.port });
|
|
168
169
|
if (mdbUrl)
|
package/lib/runner/dispatcher.js
CHANGED
|
@@ -158,7 +158,14 @@ class Dispatcher {
|
|
|
158
158
|
_createWorker(testGroup, parallelIndex, loaderData) {
|
|
159
159
|
const projectConfig = this._config.projects.find((p) => p.id === testGroup.projectId);
|
|
160
160
|
const outputDir = projectConfig.project.outputDir;
|
|
161
|
-
const worker = new import_workerHost.WorkerHost(testGroup,
|
|
161
|
+
const worker = new import_workerHost.WorkerHost(testGroup, {
|
|
162
|
+
parallelIndex,
|
|
163
|
+
config: loaderData,
|
|
164
|
+
extraEnv: this._extraEnvByProjectId.get(testGroup.projectId) || {},
|
|
165
|
+
outputDir,
|
|
166
|
+
pauseOnError: this._failureTracker.pauseOnError(),
|
|
167
|
+
pauseAtEnd: this._failureTracker.pauseAtEnd()
|
|
168
|
+
});
|
|
162
169
|
const handleOutput = (params) => {
|
|
163
170
|
const chunk = chunkFromParams(params);
|
|
164
171
|
if (worker.didFail()) {
|
|
@@ -27,6 +27,7 @@ class FailureTracker {
|
|
|
27
27
|
this._failureCount = 0;
|
|
28
28
|
this._hasWorkerErrors = false;
|
|
29
29
|
this._pauseOnError = options?.pauseOnError ?? false;
|
|
30
|
+
this._pauseAtEnd = options?.pauseAtEnd ?? false;
|
|
30
31
|
}
|
|
31
32
|
onRootSuite(rootSuite) {
|
|
32
33
|
this._rootSuite = rootSuite;
|
|
@@ -41,6 +42,9 @@ class FailureTracker {
|
|
|
41
42
|
pauseOnError() {
|
|
42
43
|
return this._pauseOnError;
|
|
43
44
|
}
|
|
45
|
+
pauseAtEnd() {
|
|
46
|
+
return this._pauseAtEnd;
|
|
47
|
+
}
|
|
44
48
|
hasReachedMaxFailures() {
|
|
45
49
|
return this.maxFailures() > 0 && this._failureCount >= this.maxFailures();
|
|
46
50
|
}
|
package/lib/runner/testRunner.js
CHANGED
|
@@ -71,6 +71,7 @@ class TestRunner extends import_events.default {
|
|
|
71
71
|
});
|
|
72
72
|
}
|
|
73
73
|
async initialize(params) {
|
|
74
|
+
(0, import_utils.setPlaywrightTestProcessEnv)();
|
|
74
75
|
this._watchTestDirs = !!params.watchTestDirs;
|
|
75
76
|
this._populateDependenciesOnList = !!params.populateDependenciesOnList;
|
|
76
77
|
}
|
|
@@ -270,7 +271,7 @@ class TestRunner extends import_events.default {
|
|
|
270
271
|
(0, import_tasks.createLoadTask)("out-of-process", { filterOnly: true, failOnLoadErrors: false, doNotRunDepsOutsideProjectFilter: true }),
|
|
271
272
|
...(0, import_tasks.createRunTestsTasks)(config)
|
|
272
273
|
];
|
|
273
|
-
const testRun = new import_tasks.TestRun(config, reporter, { pauseOnError: params.pauseOnError });
|
|
274
|
+
const testRun = new import_tasks.TestRun(config, reporter, { pauseOnError: params.pauseOnError, pauseAtEnd: params.pauseAtEnd });
|
|
274
275
|
const run = (0, import_tasks.runTasks)(testRun, tasks, 0, stop).then(async (status) => {
|
|
275
276
|
this._testRun = void 0;
|
|
276
277
|
return status;
|
|
@@ -352,6 +353,7 @@ async function resolveCtDirs(config) {
|
|
|
352
353
|
};
|
|
353
354
|
}
|
|
354
355
|
async function runAllTestsWithConfig(config) {
|
|
356
|
+
(0, import_utils.setPlaywrightTestProcessEnv)();
|
|
355
357
|
const listOnly = config.cliListOnly;
|
|
356
358
|
(0, import_gitCommitInfoPlugin.addGitCommitInfoPlugin)(config);
|
|
357
359
|
(0, import_webServerPlugin.webServerPluginsForConfig)(config).forEach((p) => config.plugins.push({ factory: p }));
|
package/lib/runner/workerHost.js
CHANGED
|
@@ -39,25 +39,26 @@ var import_ipc = require("../common/ipc");
|
|
|
39
39
|
var import_folders = require("../isomorphic/folders");
|
|
40
40
|
let lastWorkerIndex = 0;
|
|
41
41
|
class WorkerHost extends import_processHost.ProcessHost {
|
|
42
|
-
constructor(testGroup,
|
|
42
|
+
constructor(testGroup, options) {
|
|
43
43
|
const workerIndex = lastWorkerIndex++;
|
|
44
44
|
super(require.resolve("../worker/workerMain.js"), `worker-${workerIndex}`, {
|
|
45
|
-
...extraEnv,
|
|
45
|
+
...options.extraEnv,
|
|
46
46
|
FORCE_COLOR: "1",
|
|
47
47
|
DEBUG_COLORS: process.env.DEBUG_COLORS === void 0 ? "1" : process.env.DEBUG_COLORS
|
|
48
48
|
});
|
|
49
49
|
this._didFail = false;
|
|
50
50
|
this.workerIndex = workerIndex;
|
|
51
|
-
this.parallelIndex = parallelIndex;
|
|
51
|
+
this.parallelIndex = options.parallelIndex;
|
|
52
52
|
this._hash = testGroup.workerHash;
|
|
53
53
|
this._params = {
|
|
54
54
|
workerIndex: this.workerIndex,
|
|
55
|
-
parallelIndex,
|
|
55
|
+
parallelIndex: options.parallelIndex,
|
|
56
56
|
repeatEachIndex: testGroup.repeatEachIndex,
|
|
57
57
|
projectId: testGroup.projectId,
|
|
58
|
-
config,
|
|
59
|
-
artifactsDir: import_path.default.join(outputDir, (0, import_folders.artifactsFolderName)(workerIndex)),
|
|
60
|
-
pauseOnError
|
|
58
|
+
config: options.config,
|
|
59
|
+
artifactsDir: import_path.default.join(options.outputDir, (0, import_folders.artifactsFolderName)(workerIndex)),
|
|
60
|
+
pauseOnError: options.pauseOnError,
|
|
61
|
+
pauseAtEnd: options.pauseAtEnd
|
|
61
62
|
};
|
|
62
63
|
}
|
|
63
64
|
async start() {
|
package/lib/worker/testInfo.js
CHANGED
|
@@ -447,6 +447,9 @@ ${(0, import_utils.stringifyStackFrames)(step.boxedStack).join("\n")}`;
|
|
|
447
447
|
_pauseOnError() {
|
|
448
448
|
return this._workerParams.pauseOnError;
|
|
449
449
|
}
|
|
450
|
+
_pauseAtEnd() {
|
|
451
|
+
return this._workerParams.pauseAtEnd;
|
|
452
|
+
}
|
|
450
453
|
}
|
|
451
454
|
class TestStepInfoImpl {
|
|
452
455
|
constructor(testInfo, stepId, title, parentStep) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "playwright",
|
|
3
|
-
"version": "1.56.0-alpha-2025-09-
|
|
3
|
+
"version": "1.56.0-alpha-2025-09-12",
|
|
4
4
|
"description": "A high-level API to automate web browsers",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -64,7 +64,7 @@
|
|
|
64
64
|
},
|
|
65
65
|
"license": "Apache-2.0",
|
|
66
66
|
"dependencies": {
|
|
67
|
-
"playwright-core": "1.56.0-alpha-2025-09-
|
|
67
|
+
"playwright-core": "1.56.0-alpha-2025-09-12"
|
|
68
68
|
},
|
|
69
69
|
"optionalDependencies": {
|
|
70
70
|
"fsevents": "2.3.2"
|