playwright 1.56.0-alpha-2025-09-25 → 1.56.0-alpha-1758839353000
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/lib/agents/generator.md +1 -0
- package/lib/mcp/sdk/bundle.js +3 -0
- package/lib/mcp/sdk/mdb.js +19 -4
- package/lib/mcp/sdk/server.js +37 -2
- package/lib/mcp/test/streams.js +5 -7
- package/lib/mcp/test/testBackend.js +5 -5
- package/lib/mcp/test/testContext.js +2 -1
- package/lib/mcp/test/testTools.js +59 -49
- package/lib/mcpBundleImpl.js +7 -7
- package/lib/program.js +6 -4
- package/lib/reporters/list.js +8 -4
- package/lib/runner/testRunner.js +1 -1
- package/package.json +2 -2
package/lib/agents/generator.md
CHANGED
|
@@ -51,6 +51,7 @@ Your process is methodical and thorough:
|
|
|
51
51
|
@playwright/test source code that follows following convention:
|
|
52
52
|
|
|
53
53
|
- One file per scenario, one test in a file
|
|
54
|
+
- Use seed test content (copyright, structure) to emit consistent tests.
|
|
54
55
|
- File name must be fs-friendly scenario name
|
|
55
56
|
- Test must be placed in a describe matching the top-level test plan item
|
|
56
57
|
- Test title must match the scenario name
|
package/lib/mcp/sdk/bundle.js
CHANGED
|
@@ -33,6 +33,7 @@ __export(bundle_exports, {
|
|
|
33
33
|
ListRootsRequestSchema: () => ListRootsRequestSchema,
|
|
34
34
|
ListToolsRequestSchema: () => ListToolsRequestSchema,
|
|
35
35
|
PingRequestSchema: () => PingRequestSchema,
|
|
36
|
+
ProgressNotificationSchema: () => ProgressNotificationSchema,
|
|
36
37
|
SSEServerTransport: () => SSEServerTransport,
|
|
37
38
|
Server: () => Server,
|
|
38
39
|
StdioClientTransport: () => StdioClientTransport,
|
|
@@ -54,6 +55,7 @@ const StreamableHTTPServerTransport = bundle.StreamableHTTPServerTransport;
|
|
|
54
55
|
const StreamableHTTPClientTransport = bundle.StreamableHTTPClientTransport;
|
|
55
56
|
const CallToolRequestSchema = bundle.CallToolRequestSchema;
|
|
56
57
|
const ListRootsRequestSchema = bundle.ListRootsRequestSchema;
|
|
58
|
+
const ProgressNotificationSchema = bundle.ProgressNotificationSchema;
|
|
57
59
|
const ListToolsRequestSchema = bundle.ListToolsRequestSchema;
|
|
58
60
|
const PingRequestSchema = bundle.PingRequestSchema;
|
|
59
61
|
const z = bundle.z;
|
|
@@ -64,6 +66,7 @@ const z = bundle.z;
|
|
|
64
66
|
ListRootsRequestSchema,
|
|
65
67
|
ListToolsRequestSchema,
|
|
66
68
|
PingRequestSchema,
|
|
69
|
+
ProgressNotificationSchema,
|
|
67
70
|
SSEServerTransport,
|
|
68
71
|
Server,
|
|
69
72
|
StdioClientTransport,
|
package/lib/mcp/sdk/mdb.js
CHANGED
|
@@ -46,6 +46,7 @@ const z = mcpBundle.z;
|
|
|
46
46
|
class MDBBackend {
|
|
47
47
|
constructor(topLevelBackend) {
|
|
48
48
|
this._stack = [];
|
|
49
|
+
this._progress = [];
|
|
49
50
|
this._topLevelBackend = topLevelBackend;
|
|
50
51
|
}
|
|
51
52
|
async initialize(server, clientInfo) {
|
|
@@ -77,15 +78,22 @@ class MDBBackend {
|
|
|
77
78
|
entry.resultPromise = resultPromise;
|
|
78
79
|
client.callTool({
|
|
79
80
|
name,
|
|
80
|
-
arguments: args
|
|
81
|
+
arguments: args,
|
|
82
|
+
_meta: {
|
|
83
|
+
progressToken: name + "@" + (0, import_utils.createGuid)().slice(0, 8)
|
|
84
|
+
}
|
|
81
85
|
}).then((result2) => {
|
|
82
86
|
resultPromise.resolve(result2);
|
|
83
|
-
}).catch((e) =>
|
|
87
|
+
}).catch((e) => {
|
|
88
|
+
resultPromise.resolve({ content: [{ type: "text", text: String(e) }], isError: true });
|
|
89
|
+
});
|
|
84
90
|
const result = await Promise.race([interruptPromise, resultPromise]);
|
|
85
91
|
if (interruptPromise.isDone())
|
|
86
92
|
mdbDebug("client call intercepted", result);
|
|
87
93
|
else
|
|
88
94
|
mdbDebug("client call result", result);
|
|
95
|
+
result.content.unshift(...this._progress);
|
|
96
|
+
this._progress.length = 0;
|
|
89
97
|
return result;
|
|
90
98
|
}
|
|
91
99
|
async _client() {
|
|
@@ -106,6 +114,13 @@ class MDBBackend {
|
|
|
106
114
|
const client = new mcpBundle.Client({ name: "Pushing client", version: "0.0.0" }, { capabilities: { roots: {} } });
|
|
107
115
|
client.setRequestHandler(mcpBundle.ListRootsRequestSchema, () => ({ roots: this._clientInfo?.roots || [] }));
|
|
108
116
|
client.setRequestHandler(mcpBundle.PingRequestSchema, () => ({}));
|
|
117
|
+
client.setNotificationHandler(mcpBundle.ProgressNotificationSchema, (notification) => {
|
|
118
|
+
if (notification.method === "notifications/progress") {
|
|
119
|
+
const { message } = notification.params;
|
|
120
|
+
if (message)
|
|
121
|
+
this._progress.push({ type: "text", text: message });
|
|
122
|
+
}
|
|
123
|
+
});
|
|
109
124
|
await client.connect(transport);
|
|
110
125
|
mdbDebug("connected to the new client");
|
|
111
126
|
const { tools } = await client.listTools();
|
|
@@ -189,8 +204,8 @@ class ServerBackendWithCloseListener {
|
|
|
189
204
|
async listTools() {
|
|
190
205
|
return this._backend.listTools();
|
|
191
206
|
}
|
|
192
|
-
async callTool(name, args) {
|
|
193
|
-
return this._backend.callTool(name, args);
|
|
207
|
+
async callTool(name, args, progress) {
|
|
208
|
+
return this._backend.callTool(name, args, progress);
|
|
194
209
|
}
|
|
195
210
|
serverClosed(server) {
|
|
196
211
|
this._backend.serverClosed?.(server);
|
package/lib/mcp/sdk/server.js
CHANGED
|
@@ -61,13 +61,27 @@ function createServer(name, version, backend, runHeartbeat) {
|
|
|
61
61
|
return { tools };
|
|
62
62
|
});
|
|
63
63
|
let initializePromise;
|
|
64
|
-
server.setRequestHandler(mcpBundle.CallToolRequestSchema, async (request) => {
|
|
64
|
+
server.setRequestHandler(mcpBundle.CallToolRequestSchema, async (request, extra) => {
|
|
65
65
|
serverDebug("callTool", request);
|
|
66
|
+
const progressToken = request.params._meta?.progressToken;
|
|
67
|
+
let progressCounter = 0;
|
|
68
|
+
const progress = progressToken ? (params) => {
|
|
69
|
+
extra.sendNotification({
|
|
70
|
+
method: "notifications/progress",
|
|
71
|
+
params: {
|
|
72
|
+
progressToken,
|
|
73
|
+
progress: params.progress ?? ++progressCounter,
|
|
74
|
+
total: params.total,
|
|
75
|
+
message: params.message
|
|
76
|
+
}
|
|
77
|
+
}).catch(serverDebug);
|
|
78
|
+
} : () => {
|
|
79
|
+
};
|
|
66
80
|
try {
|
|
67
81
|
if (!initializePromise)
|
|
68
82
|
initializePromise = initializeServer(server, backend, runHeartbeat);
|
|
69
83
|
await initializePromise;
|
|
70
|
-
return await backend.callTool(request.params.name, request.params.arguments || {});
|
|
84
|
+
return mergeTextParts(await backend.callTool(request.params.name, request.params.arguments || {}, progress));
|
|
71
85
|
} catch (error) {
|
|
72
86
|
return {
|
|
73
87
|
content: [{ type: "text", text: "### Result\n" + String(error) }],
|
|
@@ -145,6 +159,27 @@ function firstRootPath(clientInfo) {
|
|
|
145
159
|
const url = firstRootUri ? new URL(firstRootUri) : void 0;
|
|
146
160
|
return url ? (0, import_url.fileURLToPath)(url) : void 0;
|
|
147
161
|
}
|
|
162
|
+
function mergeTextParts(result) {
|
|
163
|
+
const content = [];
|
|
164
|
+
const testParts = [];
|
|
165
|
+
for (const part of result.content) {
|
|
166
|
+
if (part.type === "text") {
|
|
167
|
+
testParts.push(part.text);
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
if (testParts.length > 0) {
|
|
171
|
+
content.push({ type: "text", text: testParts.join("\n") });
|
|
172
|
+
testParts.length = 0;
|
|
173
|
+
}
|
|
174
|
+
content.push(part);
|
|
175
|
+
}
|
|
176
|
+
if (testParts.length > 0)
|
|
177
|
+
content.push({ type: "text", text: testParts.join("\n") });
|
|
178
|
+
return {
|
|
179
|
+
...result,
|
|
180
|
+
content
|
|
181
|
+
};
|
|
182
|
+
}
|
|
148
183
|
// Annotate the CommonJS export names for ESM import in node:
|
|
149
184
|
0 && (module.exports = {
|
|
150
185
|
connect,
|
package/lib/mcp/test/streams.js
CHANGED
|
@@ -23,17 +23,15 @@ __export(streams_exports, {
|
|
|
23
23
|
module.exports = __toCommonJS(streams_exports);
|
|
24
24
|
var import_stream = require("stream");
|
|
25
25
|
class StringWriteStream extends import_stream.Writable {
|
|
26
|
-
constructor() {
|
|
27
|
-
super(
|
|
28
|
-
this.
|
|
26
|
+
constructor(progress) {
|
|
27
|
+
super();
|
|
28
|
+
this._progress = progress;
|
|
29
29
|
}
|
|
30
30
|
_write(chunk, encoding, callback) {
|
|
31
|
-
|
|
31
|
+
const text = chunk.toString();
|
|
32
|
+
this._progress({ message: text.endsWith("\n") ? text.slice(0, -1) : text });
|
|
32
33
|
callback();
|
|
33
34
|
}
|
|
34
|
-
content() {
|
|
35
|
-
return this._chunks.join("");
|
|
36
|
-
}
|
|
37
35
|
}
|
|
38
36
|
// Annotate the CommonJS export names for ESM import in node:
|
|
39
37
|
0 && (module.exports = {
|
|
@@ -45,13 +45,13 @@ class TestServerBackend {
|
|
|
45
45
|
this._configOption = configOption;
|
|
46
46
|
}
|
|
47
47
|
async initialize(server, clientInfo) {
|
|
48
|
+
const rootPath = mcp.firstRootPath(clientInfo);
|
|
48
49
|
if (this._configOption) {
|
|
49
|
-
this._context.
|
|
50
|
+
this._context.initialize(rootPath, (0, import_configLoader.resolveConfigLocation)(this._configOption));
|
|
50
51
|
return;
|
|
51
52
|
}
|
|
52
|
-
const rootPath = mcp.firstRootPath(clientInfo);
|
|
53
53
|
if (rootPath) {
|
|
54
|
-
this._context.
|
|
54
|
+
this._context.initialize(rootPath, (0, import_configLoader.resolveConfigLocation)(rootPath));
|
|
55
55
|
return;
|
|
56
56
|
}
|
|
57
57
|
throw new Error("No config option or MCP root path provided");
|
|
@@ -62,12 +62,12 @@ class TestServerBackend {
|
|
|
62
62
|
...import_tools.browserTools.map((tool) => mcp.toMcpTool(tool.schema))
|
|
63
63
|
];
|
|
64
64
|
}
|
|
65
|
-
async callTool(name, args) {
|
|
65
|
+
async callTool(name, args, progress) {
|
|
66
66
|
const tool = this._tools.find((tool2) => tool2.schema.name === name);
|
|
67
67
|
if (!tool)
|
|
68
68
|
throw new Error(`Tool not found: ${name}. Available tools: ${this._tools.map((tool2) => tool2.schema.name).join(", ")}`);
|
|
69
69
|
const parsedArguments = tool.schema.inputSchema.parse(args || {});
|
|
70
|
-
return await tool.handle(this._context, parsedArguments);
|
|
70
|
+
return await tool.handle(this._context, parsedArguments, progress);
|
|
71
71
|
}
|
|
72
72
|
serverClosed() {
|
|
73
73
|
void this._context.close();
|
|
@@ -26,8 +26,9 @@ class TestContext {
|
|
|
26
26
|
constructor(options) {
|
|
27
27
|
this.options = options;
|
|
28
28
|
}
|
|
29
|
-
|
|
29
|
+
initialize(rootPath, configLocation) {
|
|
30
30
|
this.configLocation = configLocation;
|
|
31
|
+
this.rootPath = rootPath || configLocation.configDir;
|
|
31
32
|
}
|
|
32
33
|
async createTestRunner() {
|
|
33
34
|
if (this._testRunner)
|
|
@@ -44,6 +44,7 @@ var import_listModeReporter = __toESM(require("../../reporters/listModeReporter"
|
|
|
44
44
|
var import_projectUtils = require("../../runner/projectUtils");
|
|
45
45
|
var import_testTool = require("./testTool");
|
|
46
46
|
var import_streams = require("./streams");
|
|
47
|
+
var import_util = require("../../util");
|
|
47
48
|
const listTests = (0, import_testTool.defineTestTool)({
|
|
48
49
|
schema: {
|
|
49
50
|
name: "test_list",
|
|
@@ -52,14 +53,12 @@ const listTests = (0, import_testTool.defineTestTool)({
|
|
|
52
53
|
inputSchema: import_bundle.z.object({}),
|
|
53
54
|
type: "readOnly"
|
|
54
55
|
},
|
|
55
|
-
handle: async (context) => {
|
|
56
|
-
const { screen
|
|
56
|
+
handle: async (context, _, progress) => {
|
|
57
|
+
const { screen } = createScreen(progress);
|
|
57
58
|
const reporter = new import_listModeReporter.default({ screen, includeTestId: true });
|
|
58
59
|
const testRunner = await context.createTestRunner();
|
|
59
60
|
await testRunner.listTests(reporter, {});
|
|
60
|
-
return {
|
|
61
|
-
content: [{ type: "text", text: stream.content() }]
|
|
62
|
-
};
|
|
61
|
+
return { content: [] };
|
|
63
62
|
}
|
|
64
63
|
});
|
|
65
64
|
const runTests = (0, import_testTool.defineTestTool)({
|
|
@@ -73,22 +72,17 @@ const runTests = (0, import_testTool.defineTestTool)({
|
|
|
73
72
|
}),
|
|
74
73
|
type: "readOnly"
|
|
75
74
|
},
|
|
76
|
-
handle: async (context, params) => {
|
|
77
|
-
const { screen
|
|
75
|
+
handle: async (context, params, progress) => {
|
|
76
|
+
const { screen } = createScreen(progress);
|
|
78
77
|
const configDir = context.configLocation.configDir;
|
|
79
|
-
const reporter = new import_list.default({ configDir, screen, includeTestId: true });
|
|
78
|
+
const reporter = new import_list.default({ configDir, screen, includeTestId: true, prefixStdio: "out" });
|
|
80
79
|
const testRunner = await context.createTestRunner();
|
|
81
80
|
await testRunner.runTests(reporter, {
|
|
82
81
|
locations: params.locations,
|
|
83
82
|
projects: params.projects,
|
|
84
83
|
disableConfigReporters: true
|
|
85
84
|
});
|
|
86
|
-
|
|
87
|
-
return {
|
|
88
|
-
content: [
|
|
89
|
-
{ type: "text", text }
|
|
90
|
-
]
|
|
91
|
-
};
|
|
85
|
+
return { content: [] };
|
|
92
86
|
}
|
|
93
87
|
});
|
|
94
88
|
const debugTest = (0, import_testTool.defineTestTool)({
|
|
@@ -104,12 +98,12 @@ const debugTest = (0, import_testTool.defineTestTool)({
|
|
|
104
98
|
}),
|
|
105
99
|
type: "readOnly"
|
|
106
100
|
},
|
|
107
|
-
handle: async (context, params) => {
|
|
108
|
-
const { screen
|
|
101
|
+
handle: async (context, params, progress) => {
|
|
102
|
+
const { screen } = createScreen(progress);
|
|
109
103
|
const configDir = context.configLocation.configDir;
|
|
110
|
-
const reporter = new import_list.default({ configDir, screen });
|
|
104
|
+
const reporter = new import_list.default({ configDir, screen, includeTestId: true, prefixStdio: "out" });
|
|
111
105
|
const testRunner = await context.createTestRunner();
|
|
112
|
-
|
|
106
|
+
await testRunner.runTests(reporter, {
|
|
113
107
|
headed: !context.options?.headless,
|
|
114
108
|
testIds: [params.test.id],
|
|
115
109
|
// For automatic recovery
|
|
@@ -118,64 +112,80 @@ const debugTest = (0, import_testTool.defineTestTool)({
|
|
|
118
112
|
pauseOnError: true,
|
|
119
113
|
disableConfigReporters: true
|
|
120
114
|
});
|
|
121
|
-
|
|
122
|
-
return {
|
|
123
|
-
content: [
|
|
124
|
-
{ type: "text", text }
|
|
125
|
-
],
|
|
126
|
-
isError: result.status !== "passed"
|
|
127
|
-
};
|
|
115
|
+
return { content: [] };
|
|
128
116
|
}
|
|
129
117
|
});
|
|
130
118
|
const setupPage = (0, import_testTool.defineTestTool)({
|
|
131
119
|
schema: {
|
|
132
120
|
name: "test_setup_page",
|
|
133
121
|
title: "Setup page",
|
|
134
|
-
description: "Setup the page for test",
|
|
122
|
+
description: "Setup the page for test.",
|
|
135
123
|
inputSchema: import_bundle.z.object({
|
|
136
124
|
project: import_bundle.z.string().optional().describe('Project to use for setup. For example: "chromium", if no project is provided uses the first project in the config.'),
|
|
137
|
-
|
|
125
|
+
seedFile: import_bundle.z.string().optional().describe('A seed file contains a single test that is used to setup the page for testing, for example: "tests/seed.spec.ts". If no seed file is provided, a default seed file is created.')
|
|
138
126
|
}),
|
|
139
127
|
type: "readOnly"
|
|
140
128
|
},
|
|
141
|
-
handle: async (context, params) => {
|
|
142
|
-
const { screen
|
|
129
|
+
handle: async (context, params, progress) => {
|
|
130
|
+
const { screen } = createScreen(progress);
|
|
143
131
|
const configDir = context.configLocation.configDir;
|
|
144
132
|
const reporter = new import_list.default({ configDir, screen });
|
|
145
133
|
const testRunner = await context.createTestRunner();
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
await import_fs.default.promises.mkdir(import_path.default.dirname(seedFile), { recursive: true });
|
|
155
|
-
await import_fs.default.promises.writeFile(seedFile, `import { test, expect } from '@playwright/test';
|
|
134
|
+
const config = await testRunner.loadConfig();
|
|
135
|
+
const project = params.project ? config.projects.find((p) => p.project.name === params.project) : (0, import_projectUtils.findTopLevelProjects)(config)[0];
|
|
136
|
+
const testDir = project?.project.testDir || configDir;
|
|
137
|
+
let seedFile;
|
|
138
|
+
if (!params.seedFile) {
|
|
139
|
+
seedFile = import_path.default.resolve(testDir, "seed.spec.ts");
|
|
140
|
+
await import_fs.default.promises.mkdir(import_path.default.dirname(seedFile), { recursive: true });
|
|
141
|
+
await import_fs.default.promises.writeFile(seedFile, `import { test, expect } from '@playwright/test';
|
|
156
142
|
|
|
157
|
-
test('
|
|
143
|
+
test.describe('Test group', () => {
|
|
144
|
+
test('seed', async ({ page }) => {
|
|
145
|
+
// generate code here.
|
|
146
|
+
});
|
|
147
|
+
});
|
|
158
148
|
`);
|
|
149
|
+
} else {
|
|
150
|
+
const candidateFiles = [];
|
|
151
|
+
candidateFiles.push(import_path.default.resolve(testDir, params.seedFile));
|
|
152
|
+
candidateFiles.push(import_path.default.resolve(configDir, params.seedFile));
|
|
153
|
+
candidateFiles.push(import_path.default.resolve(process.cwd(), params.seedFile));
|
|
154
|
+
for (const candidateFile of candidateFiles) {
|
|
155
|
+
if (await (0, import_util.fileExistsAsync)(candidateFile)) {
|
|
156
|
+
seedFile = candidateFile;
|
|
157
|
+
break;
|
|
158
|
+
}
|
|
159
159
|
}
|
|
160
|
+
if (!seedFile)
|
|
161
|
+
throw new Error("seed test not found.");
|
|
160
162
|
}
|
|
163
|
+
const seedFileContent = await import_fs.default.promises.readFile(seedFile, "utf8");
|
|
164
|
+
progress({ message: `### Seed test
|
|
165
|
+
File: ${import_path.default.relative(context.rootPath, seedFile)}
|
|
166
|
+
\`\`\`ts
|
|
167
|
+
${seedFileContent}
|
|
168
|
+
\`\`\`
|
|
169
|
+
` });
|
|
161
170
|
const result = await testRunner.runTests(reporter, {
|
|
162
171
|
headed: !context.options?.headless,
|
|
163
|
-
locations: [
|
|
172
|
+
locations: [seedFile],
|
|
164
173
|
projects: params.project ? [params.project] : void 0,
|
|
165
174
|
timeout: 0,
|
|
166
175
|
workers: 1,
|
|
167
176
|
pauseAtEnd: true,
|
|
168
|
-
disableConfigReporters: true
|
|
177
|
+
disableConfigReporters: true,
|
|
178
|
+
failOnLoadErrors: true
|
|
169
179
|
});
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
};
|
|
180
|
+
if (result.status === "passed" && !reporter.suite?.allTests().length)
|
|
181
|
+
throw new Error("seed test not found.");
|
|
182
|
+
if (result.status !== "passed")
|
|
183
|
+
throw new Error("Errors while running the seed test.");
|
|
184
|
+
return { content: [] };
|
|
175
185
|
}
|
|
176
186
|
});
|
|
177
|
-
function createScreen() {
|
|
178
|
-
const stream = new import_streams.StringWriteStream();
|
|
187
|
+
function createScreen(progress) {
|
|
188
|
+
const stream = new import_streams.StringWriteStream(progress);
|
|
179
189
|
const screen = {
|
|
180
190
|
...import_base.terminalScreen,
|
|
181
191
|
isTTY: false,
|