playwright 1.56.0-alpha-2025-09-08 → 1.56.0-alpha-1757456950000
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/index.js +4 -1
- package/lib/mcp/program.js +6 -1
- package/lib/mcp/sdk/mdb.js +2 -2
- package/lib/mcp/test/browserBackend.js +4 -2
- package/lib/mcp/test/testTools.js +4 -6
- package/lib/mcp/vscode/host.js +189 -0
- package/lib/mcp/vscode/main.js +77 -0
- package/lib/program.js +2 -1
- package/lib/runner/dispatcher.js +2 -2
- package/lib/runner/failureTracker.js +5 -1
- package/lib/runner/tasks.js +2 -2
- package/lib/runner/testRunner.js +1 -1
- package/lib/runner/workerHost.js +3 -2
- package/lib/worker/testInfo.js +4 -0
- package/lib/worker/workerMain.js +8 -2
- 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 -->
|
|
11
|
+
| Chromium <!-- GEN:chromium-version -->141.0.7390.7<!-- 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 -->141.0<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
|
|
14
14
|
|
package/lib/index.js
CHANGED
|
@@ -222,7 +222,7 @@ const playwrightFixtures = {
|
|
|
222
222
|
if ((0, import_utils.debugMode)() === "inspector")
|
|
223
223
|
testInfo._setDebugMode();
|
|
224
224
|
playwright._defaultContextOptions = _combinedContextOptions;
|
|
225
|
-
playwright._defaultContextTimeout =
|
|
225
|
+
playwright._defaultContextTimeout = testInfo._pauseOnError() ? 5e3 : actionTimeout || 0;
|
|
226
226
|
playwright._defaultContextNavigationTimeout = navigationTimeout || 0;
|
|
227
227
|
await use();
|
|
228
228
|
playwright._defaultContextOptions = void 0;
|
|
@@ -262,6 +262,9 @@ const playwrightFixtures = {
|
|
|
262
262
|
tracingGroupSteps.push(step);
|
|
263
263
|
},
|
|
264
264
|
onApiCallRecovery: (data, error, channelOwner, recoveryHandlers) => {
|
|
265
|
+
const testInfo2 = (0, import_globals.currentTestInfo)();
|
|
266
|
+
if (!testInfo2 || !testInfo2._pauseOnError())
|
|
267
|
+
return;
|
|
265
268
|
const step = data.userData;
|
|
266
269
|
if (!step)
|
|
267
270
|
return;
|
package/lib/mcp/program.js
CHANGED
|
@@ -39,8 +39,9 @@ var import_browserContextFactory = require("./browser/browserContextFactory");
|
|
|
39
39
|
var import_proxyBackend = require("./sdk/proxyBackend");
|
|
40
40
|
var import_browserServerBackend = require("./browser/browserServerBackend");
|
|
41
41
|
var import_extensionContextFactory = require("./extension/extensionContextFactory");
|
|
42
|
+
var import_host = require("./vscode/host");
|
|
42
43
|
function decorateCommand(command, version) {
|
|
43
|
-
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("--vision", "Legacy option, use --caps=vision instead").hideHelp()).action(async (options) => {
|
|
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) => {
|
|
44
45
|
setupExitWatchdog();
|
|
45
46
|
if (options.vision) {
|
|
46
47
|
console.error("The --vision option is deprecated, use --caps=vision instead");
|
|
@@ -59,6 +60,10 @@ function decorateCommand(command, version) {
|
|
|
59
60
|
await mcpServer.start(serverBackendFactory, config.server);
|
|
60
61
|
return;
|
|
61
62
|
}
|
|
63
|
+
if (options.vscode) {
|
|
64
|
+
await (0, import_host.runVSCodeTools)(config);
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
62
67
|
if (options.connectTool) {
|
|
63
68
|
const providers = [
|
|
64
69
|
{
|
package/lib/mcp/sdk/mdb.js
CHANGED
|
@@ -150,7 +150,7 @@ async function runMainBackend(backendFactory, options) {
|
|
|
150
150
|
return url;
|
|
151
151
|
await mcpServer.connect(factory, new mcpBundle.StdioServerTransport(), false);
|
|
152
152
|
}
|
|
153
|
-
async function runOnPauseBackendLoop(
|
|
153
|
+
async function runOnPauseBackendLoop(backend, introMessage) {
|
|
154
154
|
const wrappedBackend = new OnceTimeServerBackendWrapper(backend);
|
|
155
155
|
const factory = {
|
|
156
156
|
name: "on-pause-backend",
|
|
@@ -163,7 +163,7 @@ async function runOnPauseBackendLoop(mdbUrl, backend, introMessage) {
|
|
|
163
163
|
const url = mcpHttp.httpAddressToString(httpServer.address());
|
|
164
164
|
const client = new mcpBundle.Client({ name: "Internal client", version: "0.0.0" });
|
|
165
165
|
client.setRequestHandler(mcpBundle.PingRequestSchema, () => ({}));
|
|
166
|
-
const transport = new mcpBundle.StreamableHTTPClientTransport(new URL(
|
|
166
|
+
const transport = new mcpBundle.StreamableHTTPClientTransport(new URL(process.env.PLAYWRIGHT_DEBUGGER_MCP));
|
|
167
167
|
await client.connect(transport);
|
|
168
168
|
const pushToolsResult = await client.callTool({
|
|
169
169
|
name: pushToolsSchema.name,
|
|
@@ -34,6 +34,7 @@ __export(browserBackend_exports, {
|
|
|
34
34
|
module.exports = __toCommonJS(browserBackend_exports);
|
|
35
35
|
var mcp = __toESM(require("../sdk/exports"));
|
|
36
36
|
var mcpBundle = __toESM(require("../sdk/bundle"));
|
|
37
|
+
var import_globals = require("../../common/globals");
|
|
37
38
|
var import_browserTools = require("./browserTools");
|
|
38
39
|
var import_util = require("../../util");
|
|
39
40
|
const tools = [import_browserTools.snapshot, import_browserTools.pickLocator, import_browserTools.evaluate];
|
|
@@ -71,7 +72,8 @@ const doneToolSchema = mcp.defineToolSchema({
|
|
|
71
72
|
type: "destructive"
|
|
72
73
|
});
|
|
73
74
|
async function runBrowserBackendOnError(page, message) {
|
|
74
|
-
|
|
75
|
+
const testInfo = (0, import_globals.currentTestInfo)();
|
|
76
|
+
if (!testInfo || !testInfo._pauseOnError())
|
|
75
77
|
return;
|
|
76
78
|
const snapshot2 = await page._snapshotForAI();
|
|
77
79
|
const introMessage = `### Paused on error:
|
|
@@ -82,7 +84,7 @@ ${snapshot2}
|
|
|
82
84
|
|
|
83
85
|
### Task
|
|
84
86
|
Try recovering from the error prior to continuing, use following tools to recover: ${tools.map((tool) => tool.schema.name).join(", ")}`;
|
|
85
|
-
await mcp.runOnPauseBackendLoop(
|
|
87
|
+
await mcp.runOnPauseBackendLoop(new BrowserBackend(page), introMessage);
|
|
86
88
|
}
|
|
87
89
|
// Annotate the CommonJS export names for ESM import in node:
|
|
88
90
|
0 && (module.exports = {
|
|
@@ -64,7 +64,7 @@ const runTests = (0, import_testTool.defineTestTool)({
|
|
|
64
64
|
title: "Run tests",
|
|
65
65
|
description: "Run tests",
|
|
66
66
|
inputSchema: import_bundle.z.object({
|
|
67
|
-
locations: import_bundle.z.array(import_bundle.z.string()).describe('Folder, file or location to run: "test/e2e" or "test/e2e/file.spec.ts" or "test/e2e/file.spec.ts:20"'),
|
|
67
|
+
locations: import_bundle.z.array(import_bundle.z.string()).optional().describe('Folder, file or location to run: "test/e2e" or "test/e2e/file.spec.ts" or "test/e2e/file.spec.ts:20"'),
|
|
68
68
|
projects: import_bundle.z.array(import_bundle.z.string()).optional().describe('Projects to run, projects from playwright.config.ts, by default runs all projects. Running with "chromium" is a good start')
|
|
69
69
|
}),
|
|
70
70
|
type: "readOnly"
|
|
@@ -112,15 +112,13 @@ const debugTest = (0, import_testTool.defineTestTool)({
|
|
|
112
112
|
const configDir = context.configLocation.configDir;
|
|
113
113
|
const reporter = new import_list.default({ configDir, screen });
|
|
114
114
|
const testRunner = await context.createTestRunner();
|
|
115
|
-
process.env.PLAYWRIGHT_DEBUGGER_ENABLED = "1";
|
|
116
115
|
const result = await testRunner.runTests(reporter, {
|
|
117
|
-
headed:
|
|
116
|
+
headed: !context.options?.headless,
|
|
118
117
|
testIds: [params.test.id],
|
|
119
118
|
// For automatic recovery
|
|
120
119
|
timeout: 0,
|
|
121
|
-
workers: 1
|
|
122
|
-
|
|
123
|
-
process.env.PLAYWRIGHT_DEBUGGER_ENABLED = void 0;
|
|
120
|
+
workers: 1,
|
|
121
|
+
pauseOnError: true
|
|
124
122
|
});
|
|
125
123
|
const text = stream.content();
|
|
126
124
|
return {
|
|
@@ -0,0 +1,189 @@
|
|
|
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 host_exports = {};
|
|
30
|
+
__export(host_exports, {
|
|
31
|
+
runVSCodeTools: () => runVSCodeTools
|
|
32
|
+
});
|
|
33
|
+
module.exports = __toCommonJS(host_exports);
|
|
34
|
+
var import_path = __toESM(require("path"));
|
|
35
|
+
var mcpBundle = __toESM(require("../sdk/bundle"));
|
|
36
|
+
var mcpServer = __toESM(require("../sdk/server"));
|
|
37
|
+
var import_log = require("../log");
|
|
38
|
+
var import_browserServerBackend = require("../browser/browserServerBackend");
|
|
39
|
+
var import_browserContextFactory = require("../browser/browserContextFactory");
|
|
40
|
+
const packageJSON = require("../../../package.json");
|
|
41
|
+
const { z, zodToJsonSchema } = mcpBundle;
|
|
42
|
+
const contextSwitchOptions = z.object({
|
|
43
|
+
connectionString: z.string().optional().describe("The connection string to use to connect to the browser"),
|
|
44
|
+
lib: z.string().optional().describe("The library to use for the connection"),
|
|
45
|
+
debugController: z.boolean().optional().describe("Enable the debug controller")
|
|
46
|
+
});
|
|
47
|
+
class VSCodeProxyBackend {
|
|
48
|
+
constructor(_config, _defaultTransportFactory) {
|
|
49
|
+
this._config = _config;
|
|
50
|
+
this._defaultTransportFactory = _defaultTransportFactory;
|
|
51
|
+
this.name = "Playwright MCP Client Switcher";
|
|
52
|
+
this.version = packageJSON.version;
|
|
53
|
+
this._roots = [];
|
|
54
|
+
this._contextSwitchTool = this._defineContextSwitchTool();
|
|
55
|
+
}
|
|
56
|
+
async initialize(server, clientVersion, roots) {
|
|
57
|
+
this._clientVersion = clientVersion;
|
|
58
|
+
this._roots = roots;
|
|
59
|
+
const transport = await this._defaultTransportFactory(this);
|
|
60
|
+
await this._setCurrentClient(transport);
|
|
61
|
+
}
|
|
62
|
+
async listTools() {
|
|
63
|
+
const response = await this._currentClient.listTools();
|
|
64
|
+
return [
|
|
65
|
+
...response.tools,
|
|
66
|
+
this._contextSwitchTool
|
|
67
|
+
];
|
|
68
|
+
}
|
|
69
|
+
async callTool(name, args) {
|
|
70
|
+
if (name === this._contextSwitchTool.name)
|
|
71
|
+
return this._callContextSwitchTool(args);
|
|
72
|
+
return await this._currentClient.callTool({
|
|
73
|
+
name,
|
|
74
|
+
arguments: args
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
serverClosed(server) {
|
|
78
|
+
void this._currentClient?.close().catch(import_log.logUnhandledError);
|
|
79
|
+
}
|
|
80
|
+
onContext(context) {
|
|
81
|
+
this._context = context;
|
|
82
|
+
context.on("close", () => {
|
|
83
|
+
this._context = void 0;
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
async _getDebugControllerURL() {
|
|
87
|
+
if (!this._context)
|
|
88
|
+
return;
|
|
89
|
+
const browser = this._context.browser();
|
|
90
|
+
if (!browser || !browser._launchServer)
|
|
91
|
+
return;
|
|
92
|
+
if (this._browser !== browser)
|
|
93
|
+
this._browserServer = void 0;
|
|
94
|
+
if (!this._browserServer)
|
|
95
|
+
this._browserServer = await browser._launchServer({ _debugController: true });
|
|
96
|
+
const url = new URL(this._browserServer.wsEndpoint());
|
|
97
|
+
url.searchParams.set("debug-controller", "1");
|
|
98
|
+
return url.toString();
|
|
99
|
+
}
|
|
100
|
+
async _callContextSwitchTool(params) {
|
|
101
|
+
if (params.debugController) {
|
|
102
|
+
const url = await this._getDebugControllerURL();
|
|
103
|
+
const lines = [`### Result`];
|
|
104
|
+
if (url) {
|
|
105
|
+
lines.push(`URL: ${url}`);
|
|
106
|
+
lines.push(`Version: ${packageJSON.version}`);
|
|
107
|
+
} else {
|
|
108
|
+
lines.push(`No open browsers.`);
|
|
109
|
+
}
|
|
110
|
+
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
111
|
+
}
|
|
112
|
+
if (!params.connectionString || !params.lib) {
|
|
113
|
+
const transport = await this._defaultTransportFactory(this);
|
|
114
|
+
await this._setCurrentClient(transport);
|
|
115
|
+
return {
|
|
116
|
+
content: [{ type: "text", text: "### Result\nSuccessfully disconnected.\n" }]
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
await this._setCurrentClient(
|
|
120
|
+
new mcpBundle.StdioClientTransport({
|
|
121
|
+
command: process.execPath,
|
|
122
|
+
cwd: process.cwd(),
|
|
123
|
+
args: [
|
|
124
|
+
import_path.default.join(__dirname, "main.js"),
|
|
125
|
+
JSON.stringify(this._config),
|
|
126
|
+
params.connectionString,
|
|
127
|
+
params.lib
|
|
128
|
+
]
|
|
129
|
+
})
|
|
130
|
+
);
|
|
131
|
+
return {
|
|
132
|
+
content: [{ type: "text", text: "### Result\nSuccessfully connected.\n" }]
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
_defineContextSwitchTool() {
|
|
136
|
+
return {
|
|
137
|
+
name: "browser_connect",
|
|
138
|
+
description: "Do not call, this tool is used in the integration with the Playwright VS Code Extension and meant for programmatic usage only.",
|
|
139
|
+
inputSchema: zodToJsonSchema(contextSwitchOptions, { strictUnions: true }),
|
|
140
|
+
annotations: {
|
|
141
|
+
title: "Connect to a browser running in VS Code.",
|
|
142
|
+
readOnlyHint: true,
|
|
143
|
+
openWorldHint: false
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
async _setCurrentClient(transport) {
|
|
148
|
+
await this._currentClient?.close();
|
|
149
|
+
this._currentClient = void 0;
|
|
150
|
+
const client = new mcpBundle.Client(this._clientVersion);
|
|
151
|
+
client.registerCapabilities({
|
|
152
|
+
roots: {
|
|
153
|
+
listRoots: true
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
client.setRequestHandler(mcpBundle.ListRootsRequestSchema, () => ({ roots: this._roots }));
|
|
157
|
+
client.setRequestHandler(mcpBundle.PingRequestSchema, () => ({}));
|
|
158
|
+
await client.connect(transport);
|
|
159
|
+
this._currentClient = client;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
async function runVSCodeTools(config) {
|
|
163
|
+
const serverBackendFactory = {
|
|
164
|
+
name: "Playwright w/ vscode",
|
|
165
|
+
nameInConfig: "playwright-vscode",
|
|
166
|
+
version: packageJSON.version,
|
|
167
|
+
create: () => new VSCodeProxyBackend(
|
|
168
|
+
config,
|
|
169
|
+
(delegate) => mcpServer.wrapInProcess(
|
|
170
|
+
new import_browserServerBackend.BrowserServerBackend(
|
|
171
|
+
config,
|
|
172
|
+
{
|
|
173
|
+
async createContext(clientInfo, abortSignal, toolName) {
|
|
174
|
+
const context = await (0, import_browserContextFactory.contextFactory)(config).createContext(clientInfo, abortSignal, toolName);
|
|
175
|
+
delegate.onContext(context.browserContext);
|
|
176
|
+
return context;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
)
|
|
180
|
+
)
|
|
181
|
+
)
|
|
182
|
+
};
|
|
183
|
+
await mcpServer.start(serverBackendFactory, config.server);
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
187
|
+
0 && (module.exports = {
|
|
188
|
+
runVSCodeTools
|
|
189
|
+
});
|
|
@@ -0,0 +1,77 @@
|
|
|
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 __copyProps = (to, from, except, desc) => {
|
|
9
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
10
|
+
for (let key of __getOwnPropNames(from))
|
|
11
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
12
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
13
|
+
}
|
|
14
|
+
return to;
|
|
15
|
+
};
|
|
16
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
17
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
18
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
19
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
20
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
21
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
22
|
+
mod
|
|
23
|
+
));
|
|
24
|
+
var mcpBundle = __toESM(require("../sdk/bundle"));
|
|
25
|
+
var mcpServer = __toESM(require("../sdk/server"));
|
|
26
|
+
var import_browserServerBackend = require("../browser/browserServerBackend");
|
|
27
|
+
class VSCodeBrowserContextFactory {
|
|
28
|
+
constructor(_config, _playwright, _connectionString) {
|
|
29
|
+
this._config = _config;
|
|
30
|
+
this._playwright = _playwright;
|
|
31
|
+
this._connectionString = _connectionString;
|
|
32
|
+
this.name = "vscode";
|
|
33
|
+
this.description = "Connect to a browser running in the Playwright VS Code extension";
|
|
34
|
+
}
|
|
35
|
+
async createContext(clientInfo, abortSignal) {
|
|
36
|
+
let launchOptions = this._config.browser.launchOptions;
|
|
37
|
+
if (this._config.browser.userDataDir) {
|
|
38
|
+
launchOptions = {
|
|
39
|
+
...launchOptions,
|
|
40
|
+
...this._config.browser.contextOptions,
|
|
41
|
+
userDataDir: this._config.browser.userDataDir
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
const connectionString = new URL(this._connectionString);
|
|
45
|
+
connectionString.searchParams.set("launch-options", JSON.stringify(launchOptions));
|
|
46
|
+
const browserType = this._playwright.chromium;
|
|
47
|
+
const browser = await browserType.connect(connectionString.toString());
|
|
48
|
+
const context = browser.contexts()[0] ?? await browser.newContext(this._config.browser.contextOptions);
|
|
49
|
+
return {
|
|
50
|
+
browserContext: context,
|
|
51
|
+
close: async () => {
|
|
52
|
+
await browser.close();
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
async function main(config, connectionString, lib) {
|
|
58
|
+
const playwright = await import(lib).then((mod) => mod.default ?? mod);
|
|
59
|
+
const factory = new VSCodeBrowserContextFactory(config, playwright, connectionString);
|
|
60
|
+
await mcpServer.connect(
|
|
61
|
+
{
|
|
62
|
+
name: "Playwright MCP",
|
|
63
|
+
nameInConfig: "playwright-vscode",
|
|
64
|
+
create: () => new import_browserServerBackend.BrowserServerBackend(config, factory),
|
|
65
|
+
version: "unused"
|
|
66
|
+
},
|
|
67
|
+
new mcpBundle.StdioServerTransport(),
|
|
68
|
+
false
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
void (async () => {
|
|
72
|
+
await main(
|
|
73
|
+
JSON.parse(process.argv[2]),
|
|
74
|
+
process.argv[3],
|
|
75
|
+
process.argv[4]
|
|
76
|
+
);
|
|
77
|
+
})();
|
package/lib/program.js
CHANGED
|
@@ -151,6 +151,7 @@ function addBrowserMCPServerCommand(program3) {
|
|
|
151
151
|
function addTestMCPServerCommand(program3) {
|
|
152
152
|
const command = program3.command("run-test-mcp-server", { hidden: true });
|
|
153
153
|
command.description("Interact with the test runner over MCP");
|
|
154
|
+
command.option("--headless", "run browser in headless mode, headed by default");
|
|
154
155
|
command.option("-c, --config <file>", `Configuration file, or a test directory with optional "playwright.config.{m,c}?{js,ts}"`);
|
|
155
156
|
command.option("--host <host>", "host to bind server to. Default is localhost. Use 0.0.0.0 to bind to all interfaces.");
|
|
156
157
|
command.option("--port <port>", "port to listen on for SSE transport.");
|
|
@@ -160,7 +161,7 @@ function addTestMCPServerCommand(program3) {
|
|
|
160
161
|
name: "Playwright Test Runner",
|
|
161
162
|
nameInConfig: "playwright-test-runner",
|
|
162
163
|
version: packageJSON.version,
|
|
163
|
-
create: () => new import_testBackend.TestServerBackend(resolvedLocation, { muteConsole: options.port === void 0 })
|
|
164
|
+
create: () => new import_testBackend.TestServerBackend(resolvedLocation, { muteConsole: options.port === void 0, headless: options.headless })
|
|
164
165
|
};
|
|
165
166
|
const mdbUrl = await (0, import_exports.runMainBackend)(backendFactory, { port: options.port === void 0 ? void 0 : +options.port });
|
|
166
167
|
if (mdbUrl)
|
package/lib/runner/dispatcher.js
CHANGED
|
@@ -158,7 +158,7 @@ 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, parallelIndex, loaderData, this._extraEnvByProjectId.get(testGroup.projectId) || {}, outputDir);
|
|
161
|
+
const worker = new import_workerHost.WorkerHost(testGroup, parallelIndex, loaderData, this._extraEnvByProjectId.get(testGroup.projectId) || {}, outputDir, this._failureTracker.pauseOnError());
|
|
162
162
|
const handleOutput = (params) => {
|
|
163
163
|
const chunk = chunkFromParams(params);
|
|
164
164
|
if (worker.didFail()) {
|
|
@@ -363,7 +363,7 @@ class JobDispatcher {
|
|
|
363
363
|
}
|
|
364
364
|
}
|
|
365
365
|
_onDone(params) {
|
|
366
|
-
if (!this._remainingByTestId.size && !this._failedTests.size && !params.fatalErrors.length && !params.skipTestsDueToSetupFailure.length && !params.fatalUnknownTestIds && !params.unexpectedExitError) {
|
|
366
|
+
if (!this._remainingByTestId.size && !this._failedTests.size && !params.fatalErrors.length && !params.skipTestsDueToSetupFailure.length && !params.fatalUnknownTestIds && !params.unexpectedExitError && !params.stoppedDueToUnhandledErrorInTestFail) {
|
|
367
367
|
this._finished({ didFail: false });
|
|
368
368
|
return;
|
|
369
369
|
}
|
|
@@ -22,10 +22,11 @@ __export(failureTracker_exports, {
|
|
|
22
22
|
});
|
|
23
23
|
module.exports = __toCommonJS(failureTracker_exports);
|
|
24
24
|
class FailureTracker {
|
|
25
|
-
constructor(_config) {
|
|
25
|
+
constructor(_config, options) {
|
|
26
26
|
this._config = _config;
|
|
27
27
|
this._failureCount = 0;
|
|
28
28
|
this._hasWorkerErrors = false;
|
|
29
|
+
this._pauseOnError = options?.pauseOnError ?? false;
|
|
29
30
|
}
|
|
30
31
|
onRootSuite(rootSuite) {
|
|
31
32
|
this._rootSuite = rootSuite;
|
|
@@ -37,6 +38,9 @@ class FailureTracker {
|
|
|
37
38
|
onWorkerError() {
|
|
38
39
|
this._hasWorkerErrors = true;
|
|
39
40
|
}
|
|
41
|
+
pauseOnError() {
|
|
42
|
+
return this._pauseOnError;
|
|
43
|
+
}
|
|
40
44
|
hasReachedMaxFailures() {
|
|
41
45
|
return this.maxFailures() > 0 && this._failureCount >= this.maxFailures();
|
|
42
46
|
}
|
package/lib/runner/tasks.js
CHANGED
|
@@ -60,14 +60,14 @@ var import_compilationCache = require("../transform/compilationCache");
|
|
|
60
60
|
var import_util2 = require("../util");
|
|
61
61
|
const readDirAsync = (0, import_util.promisify)(import_fs.default.readdir);
|
|
62
62
|
class TestRun {
|
|
63
|
-
constructor(config, reporter) {
|
|
63
|
+
constructor(config, reporter, options) {
|
|
64
64
|
this.rootSuite = void 0;
|
|
65
65
|
this.phases = [];
|
|
66
66
|
this.projectFiles = /* @__PURE__ */ new Map();
|
|
67
67
|
this.projectSuites = /* @__PURE__ */ new Map();
|
|
68
68
|
this.config = config;
|
|
69
69
|
this.reporter = reporter;
|
|
70
|
-
this.failureTracker = new import_failureTracker.FailureTracker(config);
|
|
70
|
+
this.failureTracker = new import_failureTracker.FailureTracker(config, options);
|
|
71
71
|
}
|
|
72
72
|
}
|
|
73
73
|
async function runTasks(testRun, tasks, globalTimeout, cancelPromise) {
|
package/lib/runner/testRunner.js
CHANGED
|
@@ -270,7 +270,7 @@ class TestRunner extends import_events.default {
|
|
|
270
270
|
(0, import_tasks.createLoadTask)("out-of-process", { filterOnly: true, failOnLoadErrors: false, doNotRunDepsOutsideProjectFilter: true }),
|
|
271
271
|
...(0, import_tasks.createRunTestsTasks)(config)
|
|
272
272
|
];
|
|
273
|
-
const testRun = new import_tasks.TestRun(config, reporter);
|
|
273
|
+
const testRun = new import_tasks.TestRun(config, reporter, { pauseOnError: params.pauseOnError });
|
|
274
274
|
const run = (0, import_tasks.runTasks)(testRun, tasks, 0, stop).then(async (status) => {
|
|
275
275
|
this._testRun = void 0;
|
|
276
276
|
return status;
|
package/lib/runner/workerHost.js
CHANGED
|
@@ -39,7 +39,7 @@ 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, parallelIndex, config, extraEnv, outputDir) {
|
|
42
|
+
constructor(testGroup, parallelIndex, config, extraEnv, outputDir, pauseOnError) {
|
|
43
43
|
const workerIndex = lastWorkerIndex++;
|
|
44
44
|
super(require.resolve("../worker/workerMain.js"), `worker-${workerIndex}`, {
|
|
45
45
|
...extraEnv,
|
|
@@ -56,7 +56,8 @@ class WorkerHost extends import_processHost.ProcessHost {
|
|
|
56
56
|
repeatEachIndex: testGroup.repeatEachIndex,
|
|
57
57
|
projectId: testGroup.projectId,
|
|
58
58
|
config,
|
|
59
|
-
artifactsDir: import_path.default.join(outputDir, (0, import_folders.artifactsFolderName)(workerIndex))
|
|
59
|
+
artifactsDir: import_path.default.join(outputDir, (0, import_folders.artifactsFolderName)(workerIndex)),
|
|
60
|
+
pauseOnError
|
|
60
61
|
};
|
|
61
62
|
}
|
|
62
63
|
async start() {
|
package/lib/worker/testInfo.js
CHANGED
|
@@ -67,6 +67,7 @@ class TestInfoImpl {
|
|
|
67
67
|
this._startWallTime = Date.now();
|
|
68
68
|
this._requireFile = test?._requireFile ?? "";
|
|
69
69
|
this._uniqueSymbol = Symbol("testInfoUniqueSymbol");
|
|
70
|
+
this._workerParams = workerParams;
|
|
70
71
|
this.repeatEachIndex = workerParams.repeatEachIndex;
|
|
71
72
|
this.retry = retry;
|
|
72
73
|
this.workerIndex = workerParams.workerIndex;
|
|
@@ -443,6 +444,9 @@ ${(0, import_utils.stringifyStackFrames)(step.boxedStack).join("\n")}`;
|
|
|
443
444
|
setTimeout(timeout) {
|
|
444
445
|
this._timeoutManager.setTimeout(timeout);
|
|
445
446
|
}
|
|
447
|
+
_pauseOnError() {
|
|
448
|
+
return this._workerParams.pauseOnError;
|
|
449
|
+
}
|
|
446
450
|
}
|
|
447
451
|
class TestStepInfoImpl {
|
|
448
452
|
constructor(testInfo, stepId, title, parentStep) {
|
package/lib/worker/workerMain.js
CHANGED
|
@@ -43,6 +43,9 @@ class WorkerMain extends import_process.ProcessRunner {
|
|
|
43
43
|
this._fatalErrors = [];
|
|
44
44
|
// The stage of the full cleanup. Once "finished", we can safely stop running anything.
|
|
45
45
|
this._didRunFullCleanup = false;
|
|
46
|
+
// Whether the worker was stopped due to an unhandled error in a test marked with test.fail().
|
|
47
|
+
// This should force dispatcher to use a new worker instead.
|
|
48
|
+
this._stoppedDueToUnhandledErrorInTestFail = false;
|
|
46
49
|
// Whether the worker was requested to stop.
|
|
47
50
|
this._isStopped = false;
|
|
48
51
|
// This promise resolves once the single "run test group" call finishes.
|
|
@@ -154,8 +157,10 @@ class WorkerMain extends import_process.ProcessRunner {
|
|
|
154
157
|
}
|
|
155
158
|
const isExpectError = error instanceof Error && !!error.matcherResult;
|
|
156
159
|
const shouldContinueInThisWorker = this._currentTest.expectedStatus === "failed" && isExpectError;
|
|
157
|
-
if (!shouldContinueInThisWorker)
|
|
160
|
+
if (!shouldContinueInThisWorker) {
|
|
161
|
+
this._stoppedDueToUnhandledErrorInTestFail = true;
|
|
158
162
|
void this._stop();
|
|
163
|
+
}
|
|
159
164
|
}
|
|
160
165
|
async _loadIfNeeded() {
|
|
161
166
|
if (this._config)
|
|
@@ -204,7 +209,8 @@ class WorkerMain extends import_process.ProcessRunner {
|
|
|
204
209
|
const donePayload = {
|
|
205
210
|
fatalErrors: this._fatalErrors,
|
|
206
211
|
skipTestsDueToSetupFailure: [],
|
|
207
|
-
fatalUnknownTestIds
|
|
212
|
+
fatalUnknownTestIds,
|
|
213
|
+
stoppedDueToUnhandledErrorInTestFail: this._stoppedDueToUnhandledErrorInTestFail
|
|
208
214
|
};
|
|
209
215
|
for (const test of this._skipRemainingTestsInSuite?.allTests() || []) {
|
|
210
216
|
if (entries.has(test.id))
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "playwright",
|
|
3
|
-
"version": "1.56.0-alpha-
|
|
3
|
+
"version": "1.56.0-alpha-1757456950000",
|
|
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-
|
|
67
|
+
"playwright-core": "1.56.0-alpha-1757456950000"
|
|
68
68
|
},
|
|
69
69
|
"optionalDependencies": {
|
|
70
70
|
"fsevents": "2.3.2"
|