playwright 1.56.0-alpha-2025-09-29 → 1.56.0-alpha-1759271123000
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/generateAgents.js +9 -11
- package/lib/agents/generator.md +28 -38
- package/lib/agents/planner.md +2 -2
- package/lib/mcp/browser/tab.js +4 -4
- package/lib/mcp/browser/tools/console.js +4 -2
- package/lib/mcp/browser/tools/utils.js +8 -4
- package/lib/mcp/browser/tools.js +0 -5
- package/lib/mcp/sdk/mdb.js +21 -11
- package/lib/mcp/test/generatorTools.js +140 -0
- package/lib/mcp/test/plannerTools.js +46 -0
- package/lib/mcp/test/seed.js +73 -0
- package/lib/mcp/test/testBackend.js +18 -2
- package/lib/mcp/test/testContext.js +103 -0
- package/lib/mcp/test/testTools.js +5 -94
- package/lib/program.js +18 -5
- package/package.json +2 -2
- package/types/testReporter.d.ts +1 -1
|
@@ -183,19 +183,19 @@ async function initClaudeCodeRepo() {
|
|
|
183
183
|
}, null, 2));
|
|
184
184
|
}
|
|
185
185
|
const vscodeToolMap = /* @__PURE__ */ new Map([
|
|
186
|
-
["ls", ["listDirectory", "fileSearch"]],
|
|
187
|
-
["grep", ["textSearch"]],
|
|
188
|
-
["read", ["readFile"]],
|
|
189
|
-
["edit", ["editFiles"]],
|
|
190
|
-
["write", ["createFile", "createDirectory"]]
|
|
186
|
+
["ls", ["search/listDirectory", "search/fileSearch"]],
|
|
187
|
+
["grep", ["search/textSearch"]],
|
|
188
|
+
["read", ["search/readFile"]],
|
|
189
|
+
["edit", ["edit/editFiles"]],
|
|
190
|
+
["write", ["edit/createFile", "edit/createDirectory"]]
|
|
191
191
|
]);
|
|
192
|
-
const vscodeToolsOrder = ["createFile", "createDirectory", "editFiles", "fileSearch", "textSearch", "listDirectory", "readFile"];
|
|
193
|
-
const
|
|
192
|
+
const vscodeToolsOrder = ["edit/createFile", "edit/createDirectory", "edit/editFiles", "search/fileSearch", "search/textSearch", "search/listDirectory", "search/readFile"];
|
|
193
|
+
const vscodeMcpName = "playwright-test";
|
|
194
194
|
function saveAsVSCodeChatmode(agent) {
|
|
195
195
|
function asVscodeTool(tool) {
|
|
196
196
|
const [first, second] = tool.split("/");
|
|
197
197
|
if (second)
|
|
198
|
-
return second
|
|
198
|
+
return `${vscodeMcpName}/${second}`;
|
|
199
199
|
return vscodeToolMap.get(first) || first;
|
|
200
200
|
}
|
|
201
201
|
const tools = agent.header.tools.map(asVscodeTool).flat().sort((a, b) => {
|
|
@@ -240,9 +240,7 @@ async function initVSCodeRepo() {
|
|
|
240
240
|
mcpJson.servers["playwright-test"] = {
|
|
241
241
|
type: "stdio",
|
|
242
242
|
command: commonMcpServers.playwrightTest.command,
|
|
243
|
-
args: commonMcpServers.playwrightTest.args
|
|
244
|
-
cwd: "${workspaceFolder}",
|
|
245
|
-
env: { "PLAYWRIGHT_MCP_TOOL_PREFIX": vscodeToolPrefix }
|
|
243
|
+
args: commonMcpServers.playwrightTest.args
|
|
246
244
|
};
|
|
247
245
|
await writeFile(mcpJsonPath, JSON.stringify(mcpJson, null, 2));
|
|
248
246
|
}
|
package/lib/agents/generator.md
CHANGED
|
@@ -7,7 +7,6 @@ tools:
|
|
|
7
7
|
- ls
|
|
8
8
|
- grep
|
|
9
9
|
- read
|
|
10
|
-
- write
|
|
11
10
|
- playwright-test/browser_click
|
|
12
11
|
- playwright-test/browser_drag
|
|
13
12
|
- playwright-test/browser_evaluate
|
|
@@ -24,38 +23,29 @@ tools:
|
|
|
24
23
|
- playwright-test/browser_verify_text_visible
|
|
25
24
|
- playwright-test/browser_verify_value
|
|
26
25
|
- playwright-test/browser_wait_for
|
|
27
|
-
- playwright-test/
|
|
26
|
+
- playwright-test/generator_log_step
|
|
27
|
+
- playwright-test/generator_read_log
|
|
28
|
+
- playwright-test/generator_setup_page
|
|
29
|
+
- playwright-test/generator_write_test
|
|
28
30
|
---
|
|
29
31
|
|
|
30
32
|
You are a Playwright Test Generator, an expert in browser automation and end-to-end testing.
|
|
31
33
|
Your specialty is creating robust, reliable Playwright tests that accurately simulate user interactions and validate
|
|
32
34
|
application behavior.
|
|
33
35
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
3. **Test Code Generation**
|
|
49
|
-
|
|
50
|
-
After successfully completing the manual execution, generate clean, maintainable
|
|
51
|
-
@playwright/test source code that follows following convention:
|
|
52
|
-
|
|
53
|
-
- One file per scenario, one test in a file
|
|
54
|
-
- Use seed test content (copyright, structure) to emit consistent tests.
|
|
55
|
-
- File name must be fs-friendly scenario name
|
|
56
|
-
- Test must be placed in a describe matching the top-level test plan item
|
|
57
|
-
- Test title must match the scenario name
|
|
58
|
-
- Includes a comment with the step text before each step execution
|
|
36
|
+
# For each test you generate
|
|
37
|
+
- Obtain the test plan with all the steps and verification specification
|
|
38
|
+
- Run the `generator_setup_page` tool to set up page for the scenario
|
|
39
|
+
- For each step and verification in the scenario, do the following:
|
|
40
|
+
- Use Playwright tool to manually execute it in real-time.
|
|
41
|
+
- Immediately after each tool execution, log the step via running `generator_log_step` tool.
|
|
42
|
+
- Retrieve generator log via `generator_read_log`
|
|
43
|
+
- Immediately after reading the test log, invoke `generator_write_test` with the generated source code
|
|
44
|
+
- File should contain single test
|
|
45
|
+
- File name must be fs-friendly scenario name
|
|
46
|
+
- Test must be placed in a describe matching the top-level test plan item
|
|
47
|
+
- Test title must match the scenario name
|
|
48
|
+
- Includes a comment with the step text before each step execution
|
|
59
49
|
|
|
60
50
|
<example-generation>
|
|
61
51
|
For following plan:
|
|
@@ -89,17 +79,17 @@ Your process is methodical and thorough:
|
|
|
89
79
|
```
|
|
90
80
|
</example-generation>
|
|
91
81
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
82
|
+
# Best practices
|
|
83
|
+
- Each test has clear, descriptive assertions that validate the expected behavior
|
|
84
|
+
- Includes proper error handling and meaningful failure messages
|
|
85
|
+
- Uses Playwright best practices (page.waitForLoadState, expect.toBeVisible, etc.)
|
|
86
|
+
- Do not improvise, do not add directives that were not asked for
|
|
87
|
+
- Uses reliable locators (preferring data-testid, role-based, or text-based selectors over fragile CSS selectors)
|
|
88
|
+
- Uses local variables for locators that are used multiple times
|
|
89
|
+
- Uses explicit waits rather than arbitrary timeouts
|
|
90
|
+
- Never waits for networkidle or use other discouraged or deprecated apis
|
|
91
|
+
- Is self-contained and can run independently
|
|
92
|
+
- Is deterministic and not prone to flaky behavior
|
|
103
93
|
|
|
104
94
|
<example>
|
|
105
95
|
Context: User wants to test a login flow on their web application.
|
package/lib/agents/planner.md
CHANGED
|
@@ -25,7 +25,7 @@ tools:
|
|
|
25
25
|
- playwright-test/browser_take_screenshot
|
|
26
26
|
- playwright-test/browser_type
|
|
27
27
|
- playwright-test/browser_wait_for
|
|
28
|
-
- playwright-test/
|
|
28
|
+
- playwright-test/planner_setup_page
|
|
29
29
|
---
|
|
30
30
|
|
|
31
31
|
You are an expert web test planner with extensive experience in quality assurance, user experience testing, and test
|
|
@@ -35,7 +35,7 @@ planning.
|
|
|
35
35
|
You will:
|
|
36
36
|
|
|
37
37
|
1. **Navigate and Explore**
|
|
38
|
-
- Invoke the `
|
|
38
|
+
- Invoke the `planner_setup_page` tool once to set up page before using any other tools
|
|
39
39
|
- Explore the browser snapshot
|
|
40
40
|
- Do not take screenshots unless absolutely necessary
|
|
41
41
|
- Use browser_* tools to navigate and discover interface
|
package/lib/mcp/browser/tab.js
CHANGED
|
@@ -164,9 +164,9 @@ class Tab extends import_events.EventEmitter {
|
|
|
164
164
|
}
|
|
165
165
|
await this.waitForLoadState("load", { timeout: 5e3 });
|
|
166
166
|
}
|
|
167
|
-
async consoleMessages() {
|
|
167
|
+
async consoleMessages(type) {
|
|
168
168
|
await this._initializedPromise;
|
|
169
|
-
return this._consoleMessages;
|
|
169
|
+
return this._consoleMessages.filter((message) => type ? message.type === type : true);
|
|
170
170
|
}
|
|
171
171
|
async requests() {
|
|
172
172
|
await this._initializedPromise;
|
|
@@ -249,13 +249,13 @@ function messageToConsoleMessage(message) {
|
|
|
249
249
|
function pageErrorToConsoleMessage(errorOrValue) {
|
|
250
250
|
if (errorOrValue instanceof Error) {
|
|
251
251
|
return {
|
|
252
|
-
type:
|
|
252
|
+
type: "error",
|
|
253
253
|
text: errorOrValue.message,
|
|
254
254
|
toString: () => errorOrValue.stack || errorOrValue.message
|
|
255
255
|
};
|
|
256
256
|
}
|
|
257
257
|
return {
|
|
258
|
-
type:
|
|
258
|
+
type: "error",
|
|
259
259
|
text: String(errorOrValue),
|
|
260
260
|
toString: () => String(errorOrValue)
|
|
261
261
|
};
|
|
@@ -29,11 +29,13 @@ const console = (0, import_tool.defineTabTool)({
|
|
|
29
29
|
name: "browser_console_messages",
|
|
30
30
|
title: "Get console messages",
|
|
31
31
|
description: "Returns all console messages",
|
|
32
|
-
inputSchema: import_bundle.z.object({
|
|
32
|
+
inputSchema: import_bundle.z.object({
|
|
33
|
+
onlyErrors: import_bundle.z.boolean().optional().describe("Only return error messages")
|
|
34
|
+
}),
|
|
33
35
|
type: "readOnly"
|
|
34
36
|
},
|
|
35
37
|
handle: async (tab, params, response) => {
|
|
36
|
-
const messages = await tab.consoleMessages();
|
|
38
|
+
const messages = await tab.consoleMessages(params.onlyErrors ? "error" : void 0);
|
|
37
39
|
messages.map((message) => response.addResult(message.toString()));
|
|
38
40
|
}
|
|
39
41
|
});
|
|
@@ -33,12 +33,16 @@ async function waitForCompletion(tab, callback) {
|
|
|
33
33
|
const waitBarrier = new Promise((f) => {
|
|
34
34
|
waitCallback = f;
|
|
35
35
|
});
|
|
36
|
-
const
|
|
37
|
-
const requestFinishedListener = (request) => {
|
|
36
|
+
const responseListener = (request) => {
|
|
38
37
|
requests.delete(request);
|
|
39
38
|
if (!requests.size)
|
|
40
39
|
waitCallback();
|
|
41
40
|
};
|
|
41
|
+
const requestListener = (request) => {
|
|
42
|
+
requests.add(request);
|
|
43
|
+
void request.response().then(() => responseListener(request)).catch(() => {
|
|
44
|
+
});
|
|
45
|
+
};
|
|
42
46
|
const frameNavigateListener = (frame) => {
|
|
43
47
|
if (frame.parentFrame())
|
|
44
48
|
return;
|
|
@@ -52,12 +56,12 @@ async function waitForCompletion(tab, callback) {
|
|
|
52
56
|
waitCallback();
|
|
53
57
|
};
|
|
54
58
|
tab.page.on("request", requestListener);
|
|
55
|
-
tab.page.on("
|
|
59
|
+
tab.page.on("requestfailed", responseListener);
|
|
56
60
|
tab.page.on("framenavigated", frameNavigateListener);
|
|
57
61
|
const timeout = setTimeout(onTimeout, 1e4);
|
|
58
62
|
const dispose = () => {
|
|
59
63
|
tab.page.off("request", requestListener);
|
|
60
|
-
tab.page.off("
|
|
64
|
+
tab.page.off("requestfailed", responseListener);
|
|
61
65
|
tab.page.off("framenavigated", frameNavigateListener);
|
|
62
66
|
clearTimeout(timeout);
|
|
63
67
|
};
|
package/lib/mcp/browser/tools.js
CHANGED
|
@@ -70,11 +70,6 @@ const browserTools = [
|
|
|
70
70
|
...import_wait.default,
|
|
71
71
|
...import_verify.default
|
|
72
72
|
];
|
|
73
|
-
const customPrefix = process.env.PLAYWRIGHT_MCP_TOOL_PREFIX;
|
|
74
|
-
if (customPrefix) {
|
|
75
|
-
for (const tool of browserTools)
|
|
76
|
-
tool.schema.name = customPrefix + tool.schema.name;
|
|
77
|
-
}
|
|
78
73
|
function filteredTools(config) {
|
|
79
74
|
return browserTools.filter((tool) => tool.capability.startsWith("core") || config.capabilities?.includes(tool.capability));
|
|
80
75
|
}
|
package/lib/mcp/sdk/mdb.js
CHANGED
|
@@ -44,10 +44,11 @@ const mdbDebug = (0, import_utilsBundle.debug)("pw:mcp:mdb");
|
|
|
44
44
|
const errorsDebug = (0, import_utilsBundle.debug)("pw:mcp:errors");
|
|
45
45
|
const z = mcpBundle.z;
|
|
46
46
|
class MDBBackend {
|
|
47
|
-
constructor(topLevelBackend) {
|
|
47
|
+
constructor(topLevelBackend, allowedOnPause) {
|
|
48
48
|
this._stack = [];
|
|
49
49
|
this._progress = [];
|
|
50
50
|
this._topLevelBackend = topLevelBackend;
|
|
51
|
+
this._allowedOnPause = allowedOnPause;
|
|
51
52
|
}
|
|
52
53
|
async initialize(server, clientInfo) {
|
|
53
54
|
if (!this._clientInfo)
|
|
@@ -64,16 +65,25 @@ class MDBBackend {
|
|
|
64
65
|
return await this._pushTools(pushToolsSchema.inputSchema.parse(args || {}));
|
|
65
66
|
const interruptPromise = new import_utils.ManualPromise();
|
|
66
67
|
this._interruptPromise = interruptPromise;
|
|
67
|
-
|
|
68
|
-
|
|
68
|
+
if (!this._allowedOnPause.includes(name)) {
|
|
69
|
+
for (let i = 0; i < this._stack.length - 1; i++) {
|
|
70
|
+
if (this._stack[i].toolNames.includes(name))
|
|
71
|
+
break;
|
|
72
|
+
await this._stack[i].client.close().catch(errorsDebug);
|
|
73
|
+
this._stack.shift();
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
let entry;
|
|
77
|
+
for (let i = 0; i < this._stack.length; i++) {
|
|
78
|
+
if (this._stack[i].toolNames.includes(name)) {
|
|
79
|
+
entry = this._stack[i];
|
|
80
|
+
break;
|
|
81
|
+
}
|
|
69
82
|
mdbDebug("popping client from stack for ", name);
|
|
70
|
-
this._stack.shift();
|
|
71
|
-
await entry.client.close().catch(errorsDebug);
|
|
72
|
-
entry = this._stack[0];
|
|
73
83
|
}
|
|
74
84
|
if (!entry)
|
|
75
85
|
throw new Error(`Tool ${name} not found in the tool stack`);
|
|
76
|
-
const client =
|
|
86
|
+
const client = entry.client;
|
|
77
87
|
const resultPromise = new import_utils.ManualPromise();
|
|
78
88
|
entry.resultPromise = resultPromise;
|
|
79
89
|
client.callTool({
|
|
@@ -111,7 +121,7 @@ class MDBBackend {
|
|
|
111
121
|
}
|
|
112
122
|
async _pushClient(transport, introMessage) {
|
|
113
123
|
mdbDebug("pushing client to the stack");
|
|
114
|
-
const client = new mcpBundle.Client({ name: "
|
|
124
|
+
const client = new mcpBundle.Client({ name: "Interrupting client", version: "0.0.0" }, { capabilities: { roots: {} } });
|
|
115
125
|
client.setRequestHandler(mcpBundle.ListRootsRequestSchema, () => ({ roots: this._clientInfo?.roots || [] }));
|
|
116
126
|
client.setRequestHandler(mcpBundle.PingRequestSchema, () => ({}));
|
|
117
127
|
client.setNotificationHandler(mcpBundle.ProgressNotificationSchema, (notification) => {
|
|
@@ -147,8 +157,8 @@ const pushToolsSchema = (0, import_tool.defineToolSchema)({
|
|
|
147
157
|
}),
|
|
148
158
|
type: "readOnly"
|
|
149
159
|
});
|
|
150
|
-
async function runMainBackend(backendFactory, options) {
|
|
151
|
-
const mdbBackend = new MDBBackend(backendFactory.create());
|
|
160
|
+
async function runMainBackend(backendFactory, allowedOnPause, options) {
|
|
161
|
+
const mdbBackend = new MDBBackend(backendFactory.create(), allowedOnPause);
|
|
152
162
|
const factory = {
|
|
153
163
|
...backendFactory,
|
|
154
164
|
create: () => mdbBackend
|
|
@@ -170,7 +180,7 @@ async function runOnPauseBackendLoop(backend, introMessage) {
|
|
|
170
180
|
const httpServer = await mcpHttp.startHttpServer({ port: 0 });
|
|
171
181
|
await mcpHttp.installHttpTransport(httpServer, factory);
|
|
172
182
|
const url = mcpHttp.httpAddressToString(httpServer.address());
|
|
173
|
-
const client = new mcpBundle.Client({ name: "
|
|
183
|
+
const client = new mcpBundle.Client({ name: "Pushing client", version: "0.0.0" });
|
|
174
184
|
client.setRequestHandler(mcpBundle.PingRequestSchema, () => ({}));
|
|
175
185
|
const transport = new mcpBundle.StreamableHTTPClientTransport(new URL(process.env.PLAYWRIGHT_DEBUGGER_MCP));
|
|
176
186
|
await client.connect(transport);
|
|
@@ -0,0 +1,140 @@
|
|
|
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 generatorTools_exports = {};
|
|
30
|
+
__export(generatorTools_exports, {
|
|
31
|
+
generatorLogStep: () => generatorLogStep,
|
|
32
|
+
generatorReadLog: () => generatorReadLog,
|
|
33
|
+
generatorWriteTest: () => generatorWriteTest,
|
|
34
|
+
setupPage: () => setupPage
|
|
35
|
+
});
|
|
36
|
+
module.exports = __toCommonJS(generatorTools_exports);
|
|
37
|
+
var import_fs = __toESM(require("fs"));
|
|
38
|
+
var import_path = __toESM(require("path"));
|
|
39
|
+
var import_bundle = require("../sdk/bundle");
|
|
40
|
+
var import_testTool = require("./testTool");
|
|
41
|
+
var import_testContext = require("./testContext");
|
|
42
|
+
const setupPage = (0, import_testTool.defineTestTool)({
|
|
43
|
+
schema: {
|
|
44
|
+
name: "generator_setup_page",
|
|
45
|
+
title: "Setup generator page",
|
|
46
|
+
description: "Setup the page for test.",
|
|
47
|
+
inputSchema: import_bundle.z.object({
|
|
48
|
+
plan: import_bundle.z.string().describe("The plan for the test. This should be the actual test plan with all the steps."),
|
|
49
|
+
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.'),
|
|
50
|
+
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.')
|
|
51
|
+
}),
|
|
52
|
+
type: "readOnly"
|
|
53
|
+
},
|
|
54
|
+
handle: async (context, params, progress) => {
|
|
55
|
+
const seed = await context.getOrCreateSeedFile(params);
|
|
56
|
+
context.generatorJournal = new import_testContext.GeneratorJournal(context.rootPath, params.plan, seed);
|
|
57
|
+
await context.runSeedTest(seed.file, params.project, progress);
|
|
58
|
+
return { content: [] };
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
const generatorLogStep = (0, import_testTool.defineTestTool)({
|
|
62
|
+
schema: {
|
|
63
|
+
name: "generator_log_step",
|
|
64
|
+
title: "Log a test step",
|
|
65
|
+
description: "Log a successful test step.",
|
|
66
|
+
inputSchema: import_bundle.z.object({
|
|
67
|
+
title: import_bundle.z.string().describe("Title of the test step"),
|
|
68
|
+
code: import_bundle.z.string().describe("The code for the last step only, as a markdown code block")
|
|
69
|
+
}),
|
|
70
|
+
type: "readOnly"
|
|
71
|
+
},
|
|
72
|
+
handle: async (context, params) => {
|
|
73
|
+
if (!context.generatorJournal)
|
|
74
|
+
throw new Error(`Please setup page using "${setupPage.schema.name}" first.`);
|
|
75
|
+
context.generatorJournal.logStep(params.title, params.code);
|
|
76
|
+
return { content: [] };
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
const generatorReadLog = (0, import_testTool.defineTestTool)({
|
|
80
|
+
schema: {
|
|
81
|
+
name: "generator_read_log",
|
|
82
|
+
title: "Retrieve test log",
|
|
83
|
+
description: "Retrieve the performed test log",
|
|
84
|
+
inputSchema: import_bundle.z.object({}),
|
|
85
|
+
type: "readOnly"
|
|
86
|
+
},
|
|
87
|
+
handle: async (context) => {
|
|
88
|
+
if (!context.generatorJournal)
|
|
89
|
+
throw new Error(`Please setup page using "${setupPage.schema.name}" first.`);
|
|
90
|
+
const result = context.generatorJournal.journal();
|
|
91
|
+
return { content: [{
|
|
92
|
+
type: "text",
|
|
93
|
+
text: result
|
|
94
|
+
}] };
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
const generatorWriteTest = (0, import_testTool.defineTestTool)({
|
|
98
|
+
schema: {
|
|
99
|
+
name: "generator_write_test",
|
|
100
|
+
title: "Write test",
|
|
101
|
+
description: "Write the generated test to the test file",
|
|
102
|
+
inputSchema: import_bundle.z.object({
|
|
103
|
+
fileName: import_bundle.z.string().describe("The file to write the test to"),
|
|
104
|
+
code: import_bundle.z.string().describe("The generated test code")
|
|
105
|
+
}),
|
|
106
|
+
type: "destructive"
|
|
107
|
+
},
|
|
108
|
+
handle: async (context, params) => {
|
|
109
|
+
if (!context.generatorJournal)
|
|
110
|
+
throw new Error(`Please setup page using "${setupPage.schema.name}" first.`);
|
|
111
|
+
const testRunner = context.existingTestRunner();
|
|
112
|
+
if (!testRunner)
|
|
113
|
+
throw new Error("No test runner found, please setup page and perform actions first.");
|
|
114
|
+
const config = await testRunner.loadConfig();
|
|
115
|
+
const dirs = [];
|
|
116
|
+
for (const project of config.projects) {
|
|
117
|
+
const testDir = import_path.default.relative(context.rootPath, project.project.testDir).replace(/\\/g, "/");
|
|
118
|
+
const fileName = params.fileName.replace(/\\/g, "/");
|
|
119
|
+
if (fileName.startsWith(testDir)) {
|
|
120
|
+
await import_fs.default.promises.writeFile(import_path.default.resolve(context.rootPath, fileName), params.code);
|
|
121
|
+
return {
|
|
122
|
+
content: [{
|
|
123
|
+
type: "text",
|
|
124
|
+
text: `### Result
|
|
125
|
+
Test written to ${params.fileName}`
|
|
126
|
+
}]
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
dirs.push(testDir);
|
|
130
|
+
}
|
|
131
|
+
throw new Error(`Test file did not match any of the test dirs: ${dirs.join(", ")}`);
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
135
|
+
0 && (module.exports = {
|
|
136
|
+
generatorLogStep,
|
|
137
|
+
generatorReadLog,
|
|
138
|
+
generatorWriteTest,
|
|
139
|
+
setupPage
|
|
140
|
+
});
|
|
@@ -0,0 +1,46 @@
|
|
|
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 plannerTools_exports = {};
|
|
20
|
+
__export(plannerTools_exports, {
|
|
21
|
+
setupPage: () => setupPage
|
|
22
|
+
});
|
|
23
|
+
module.exports = __toCommonJS(plannerTools_exports);
|
|
24
|
+
var import_bundle = require("../sdk/bundle");
|
|
25
|
+
var import_testTool = require("./testTool");
|
|
26
|
+
const setupPage = (0, import_testTool.defineTestTool)({
|
|
27
|
+
schema: {
|
|
28
|
+
name: "planner_setup_page",
|
|
29
|
+
title: "Setup planner page",
|
|
30
|
+
description: "Setup the page for test planning",
|
|
31
|
+
inputSchema: import_bundle.z.object({
|
|
32
|
+
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.'),
|
|
33
|
+
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.')
|
|
34
|
+
}),
|
|
35
|
+
type: "readOnly"
|
|
36
|
+
},
|
|
37
|
+
handle: async (context, params, progress) => {
|
|
38
|
+
const seed = await context.getOrCreateSeedFile(params);
|
|
39
|
+
await context.runSeedTest(seed.file, params.project, progress);
|
|
40
|
+
return { content: [] };
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
44
|
+
0 && (module.exports = {
|
|
45
|
+
setupPage
|
|
46
|
+
});
|
|
@@ -0,0 +1,73 @@
|
|
|
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 seed_exports = {};
|
|
30
|
+
__export(seed_exports, {
|
|
31
|
+
ensureSeedTest: () => ensureSeedTest,
|
|
32
|
+
seedProject: () => seedProject
|
|
33
|
+
});
|
|
34
|
+
module.exports = __toCommonJS(seed_exports);
|
|
35
|
+
var import_fs = __toESM(require("fs"));
|
|
36
|
+
var import_path = __toESM(require("path"));
|
|
37
|
+
var import_utils = require("playwright-core/lib/utils");
|
|
38
|
+
var import_projectUtils = require("../../runner/projectUtils");
|
|
39
|
+
function seedProject(config, projectName) {
|
|
40
|
+
if (!projectName)
|
|
41
|
+
return (0, import_projectUtils.findTopLevelProjects)(config)[0];
|
|
42
|
+
const project = config.projects.find((p) => p.project.name === projectName);
|
|
43
|
+
if (!project)
|
|
44
|
+
throw new Error(`Project ${projectName} not found`);
|
|
45
|
+
return project;
|
|
46
|
+
}
|
|
47
|
+
async function ensureSeedTest(config, projectName, logNew) {
|
|
48
|
+
const project = seedProject(config, projectName);
|
|
49
|
+
const files = await (0, import_projectUtils.collectFilesForProject)(project);
|
|
50
|
+
const seed = files.find((file) => import_path.default.basename(file).includes("seed"));
|
|
51
|
+
if (seed)
|
|
52
|
+
return seed;
|
|
53
|
+
const testDir = project.project.testDir;
|
|
54
|
+
const seedFile = import_path.default.resolve(testDir, "seed.spec.ts");
|
|
55
|
+
if (logNew) {
|
|
56
|
+
console.log(`Writing file: ${import_path.default.relative(process.cwd(), seedFile)}`);
|
|
57
|
+
}
|
|
58
|
+
await (0, import_utils.mkdirIfNeeded)(seedFile);
|
|
59
|
+
await import_fs.default.promises.writeFile(seedFile, `import { test, expect } from '@playwright/test';
|
|
60
|
+
|
|
61
|
+
test.describe('Test group', () => {
|
|
62
|
+
test('seed', async ({ page }) => {
|
|
63
|
+
// generate code here.
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
`);
|
|
67
|
+
return seedFile;
|
|
68
|
+
}
|
|
69
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
70
|
+
0 && (module.exports = {
|
|
71
|
+
ensureSeedTest,
|
|
72
|
+
seedProject
|
|
73
|
+
});
|
|
@@ -33,17 +33,33 @@ __export(testBackend_exports, {
|
|
|
33
33
|
module.exports = __toCommonJS(testBackend_exports);
|
|
34
34
|
var mcp = __toESM(require("../sdk/exports"));
|
|
35
35
|
var import_testContext = require("./testContext");
|
|
36
|
-
var
|
|
36
|
+
var testTools = __toESM(require("./testTools.js"));
|
|
37
|
+
var generatorTools = __toESM(require("./generatorTools.js"));
|
|
38
|
+
var plannerTools = __toESM(require("./plannerTools.js"));
|
|
37
39
|
var import_tools = require("../browser/tools");
|
|
38
40
|
var import_configLoader = require("../../common/configLoader");
|
|
39
41
|
class TestServerBackend {
|
|
40
42
|
constructor(configOption, options) {
|
|
41
43
|
this.name = "Playwright";
|
|
42
44
|
this.version = "0.0.1";
|
|
43
|
-
this._tools = [
|
|
45
|
+
this._tools = [
|
|
46
|
+
plannerTools.setupPage,
|
|
47
|
+
generatorTools.setupPage,
|
|
48
|
+
generatorTools.generatorLogStep,
|
|
49
|
+
generatorTools.generatorReadLog,
|
|
50
|
+
generatorTools.generatorWriteTest,
|
|
51
|
+
testTools.listTests,
|
|
52
|
+
testTools.runTests,
|
|
53
|
+
testTools.debugTest
|
|
54
|
+
];
|
|
44
55
|
this._context = new import_testContext.TestContext(options);
|
|
45
56
|
this._configOption = configOption;
|
|
46
57
|
}
|
|
58
|
+
static {
|
|
59
|
+
this.allowedOnPause = [
|
|
60
|
+
generatorTools.generatorLogStep.schema.name
|
|
61
|
+
];
|
|
62
|
+
}
|
|
47
63
|
async initialize(server, clientInfo) {
|
|
48
64
|
const rootPath = mcp.firstRootPath(clientInfo);
|
|
49
65
|
if (this._configOption) {
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
2
3
|
var __defProp = Object.defineProperty;
|
|
3
4
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
5
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
5
7
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
8
|
var __export = (target, all) => {
|
|
7
9
|
for (var name in all)
|
|
@@ -15,13 +17,54 @@ var __copyProps = (to, from, except, desc) => {
|
|
|
15
17
|
}
|
|
16
18
|
return to;
|
|
17
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
|
+
));
|
|
18
28
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
29
|
var testContext_exports = {};
|
|
20
30
|
__export(testContext_exports, {
|
|
31
|
+
GeneratorJournal: () => GeneratorJournal,
|
|
21
32
|
TestContext: () => TestContext
|
|
22
33
|
});
|
|
23
34
|
module.exports = __toCommonJS(testContext_exports);
|
|
35
|
+
var import_fs = __toESM(require("fs"));
|
|
36
|
+
var import_path = __toESM(require("path"));
|
|
37
|
+
var import_utils = require("playwright-core/lib/utils");
|
|
38
|
+
var import_base = require("../../reporters/base");
|
|
39
|
+
var import_list = __toESM(require("../../reporters/list"));
|
|
40
|
+
var import_streams = require("./streams");
|
|
41
|
+
var import_util = require("../../util");
|
|
24
42
|
var import_testRunner = require("../../runner/testRunner");
|
|
43
|
+
var import_seed = require("./seed");
|
|
44
|
+
class GeneratorJournal {
|
|
45
|
+
constructor(rootPath, plan, seed) {
|
|
46
|
+
this._rootPath = rootPath;
|
|
47
|
+
this._plan = plan;
|
|
48
|
+
this._seed = seed;
|
|
49
|
+
this._steps = [];
|
|
50
|
+
}
|
|
51
|
+
logStep(title, code) {
|
|
52
|
+
this._steps.push({ title, code });
|
|
53
|
+
}
|
|
54
|
+
journal() {
|
|
55
|
+
const result = [];
|
|
56
|
+
result.push(`# Plan`);
|
|
57
|
+
result.push(this._plan);
|
|
58
|
+
result.push(`# Seed file: ${import_path.default.relative(this._rootPath, this._seed.file)}`);
|
|
59
|
+
result.push("```ts");
|
|
60
|
+
result.push(this._seed.content);
|
|
61
|
+
result.push("```");
|
|
62
|
+
result.push(`# Steps`);
|
|
63
|
+
result.push(this._steps.map((step) => `### ${step.title}
|
|
64
|
+
${step.code}`).join("\n\n"));
|
|
65
|
+
return result.join("\n\n");
|
|
66
|
+
}
|
|
67
|
+
}
|
|
25
68
|
class TestContext {
|
|
26
69
|
constructor(options) {
|
|
27
70
|
this.options = options;
|
|
@@ -30,6 +73,9 @@ class TestContext {
|
|
|
30
73
|
this.configLocation = configLocation;
|
|
31
74
|
this.rootPath = rootPath || configLocation.configDir;
|
|
32
75
|
}
|
|
76
|
+
existingTestRunner() {
|
|
77
|
+
return this._testRunner;
|
|
78
|
+
}
|
|
33
79
|
async createTestRunner() {
|
|
34
80
|
if (this._testRunner)
|
|
35
81
|
await this._testRunner.stopTests();
|
|
@@ -42,10 +88,67 @@ class TestContext {
|
|
|
42
88
|
this._testRunner = testRunner;
|
|
43
89
|
return testRunner;
|
|
44
90
|
}
|
|
91
|
+
async getOrCreateSeedFile(params) {
|
|
92
|
+
const configDir = this.configLocation.configDir;
|
|
93
|
+
const testRunner = await this.createTestRunner();
|
|
94
|
+
const config = await testRunner.loadConfig();
|
|
95
|
+
let seedFile;
|
|
96
|
+
if (!params.seedFile) {
|
|
97
|
+
seedFile = await (0, import_seed.ensureSeedTest)(config, params.project, false);
|
|
98
|
+
} else {
|
|
99
|
+
const candidateFiles = [];
|
|
100
|
+
const testDir = (0, import_seed.seedProject)(config, params.project).project.testDir;
|
|
101
|
+
candidateFiles.push(import_path.default.resolve(testDir, params.seedFile));
|
|
102
|
+
candidateFiles.push(import_path.default.resolve(configDir, params.seedFile));
|
|
103
|
+
candidateFiles.push(import_path.default.resolve(this.rootPath, params.seedFile));
|
|
104
|
+
for (const candidateFile of candidateFiles) {
|
|
105
|
+
if (await (0, import_util.fileExistsAsync)(candidateFile)) {
|
|
106
|
+
seedFile = candidateFile;
|
|
107
|
+
break;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
if (!seedFile)
|
|
111
|
+
throw new Error("seed test not found.");
|
|
112
|
+
}
|
|
113
|
+
const seedFileContent = await import_fs.default.promises.readFile(seedFile, "utf8");
|
|
114
|
+
return { file: seedFile, content: seedFileContent };
|
|
115
|
+
}
|
|
116
|
+
async runSeedTest(seedFile, projectName, progress) {
|
|
117
|
+
const { screen } = this.createScreen(progress);
|
|
118
|
+
const configDir = this.configLocation.configDir;
|
|
119
|
+
const reporter = new import_list.default({ configDir, screen });
|
|
120
|
+
const testRunner = await this.createTestRunner();
|
|
121
|
+
const result = await testRunner.runTests(reporter, {
|
|
122
|
+
headed: !this.options?.headless,
|
|
123
|
+
locations: ["/" + (0, import_utils.escapeRegExp)(seedFile) + "/"],
|
|
124
|
+
projects: projectName ? [projectName] : void 0,
|
|
125
|
+
timeout: 0,
|
|
126
|
+
workers: 1,
|
|
127
|
+
pauseAtEnd: true,
|
|
128
|
+
disableConfigReporters: true,
|
|
129
|
+
failOnLoadErrors: true
|
|
130
|
+
});
|
|
131
|
+
if (result.status === "passed" && !reporter.suite?.allTests().length)
|
|
132
|
+
throw new Error("seed test not found.");
|
|
133
|
+
if (result.status !== "passed")
|
|
134
|
+
throw new Error("Errors while running the seed test.");
|
|
135
|
+
}
|
|
136
|
+
createScreen(progress) {
|
|
137
|
+
const stream = new import_streams.StringWriteStream(progress);
|
|
138
|
+
const screen = {
|
|
139
|
+
...import_base.terminalScreen,
|
|
140
|
+
isTTY: false,
|
|
141
|
+
colors: import_utils.noColors,
|
|
142
|
+
stdout: stream,
|
|
143
|
+
stderr: stream
|
|
144
|
+
};
|
|
145
|
+
return { screen, stream };
|
|
146
|
+
}
|
|
45
147
|
async close() {
|
|
46
148
|
}
|
|
47
149
|
}
|
|
48
150
|
// Annotate the CommonJS export names for ESM import in node:
|
|
49
151
|
0 && (module.exports = {
|
|
152
|
+
GeneratorJournal,
|
|
50
153
|
TestContext
|
|
51
154
|
});
|
|
@@ -30,21 +30,13 @@ var testTools_exports = {};
|
|
|
30
30
|
__export(testTools_exports, {
|
|
31
31
|
debugTest: () => debugTest,
|
|
32
32
|
listTests: () => listTests,
|
|
33
|
-
runTests: () => runTests
|
|
34
|
-
setupPage: () => setupPage
|
|
33
|
+
runTests: () => runTests
|
|
35
34
|
});
|
|
36
35
|
module.exports = __toCommonJS(testTools_exports);
|
|
37
|
-
var import_fs = __toESM(require("fs"));
|
|
38
|
-
var import_path = __toESM(require("path"));
|
|
39
|
-
var import_utils = require("playwright-core/lib/utils");
|
|
40
36
|
var import_bundle = require("../sdk/bundle");
|
|
41
|
-
var import_base = require("../../reporters/base");
|
|
42
37
|
var import_list = __toESM(require("../../reporters/list"));
|
|
43
38
|
var import_listModeReporter = __toESM(require("../../reporters/listModeReporter"));
|
|
44
|
-
var import_projectUtils = require("../../runner/projectUtils");
|
|
45
39
|
var import_testTool = require("./testTool");
|
|
46
|
-
var import_streams = require("./streams");
|
|
47
|
-
var import_util = require("../../util");
|
|
48
40
|
const listTests = (0, import_testTool.defineTestTool)({
|
|
49
41
|
schema: {
|
|
50
42
|
name: "test_list",
|
|
@@ -54,7 +46,7 @@ const listTests = (0, import_testTool.defineTestTool)({
|
|
|
54
46
|
type: "readOnly"
|
|
55
47
|
},
|
|
56
48
|
handle: async (context, _, progress) => {
|
|
57
|
-
const { screen } = createScreen(progress);
|
|
49
|
+
const { screen } = context.createScreen(progress);
|
|
58
50
|
const reporter = new import_listModeReporter.default({ screen, includeTestId: true });
|
|
59
51
|
const testRunner = await context.createTestRunner();
|
|
60
52
|
await testRunner.listTests(reporter, {});
|
|
@@ -73,7 +65,7 @@ const runTests = (0, import_testTool.defineTestTool)({
|
|
|
73
65
|
type: "readOnly"
|
|
74
66
|
},
|
|
75
67
|
handle: async (context, params, progress) => {
|
|
76
|
-
const { screen } = createScreen(progress);
|
|
68
|
+
const { screen } = context.createScreen(progress);
|
|
77
69
|
const configDir = context.configLocation.configDir;
|
|
78
70
|
const reporter = new import_list.default({ configDir, screen, includeTestId: true, prefixStdio: "out" });
|
|
79
71
|
const testRunner = await context.createTestRunner();
|
|
@@ -99,7 +91,7 @@ const debugTest = (0, import_testTool.defineTestTool)({
|
|
|
99
91
|
type: "readOnly"
|
|
100
92
|
},
|
|
101
93
|
handle: async (context, params, progress) => {
|
|
102
|
-
const { screen } = createScreen(progress);
|
|
94
|
+
const { screen } = context.createScreen(progress);
|
|
103
95
|
const configDir = context.configLocation.configDir;
|
|
104
96
|
const reporter = new import_list.default({ configDir, screen, includeTestId: true, prefixStdio: "out" });
|
|
105
97
|
const testRunner = await context.createTestRunner();
|
|
@@ -115,90 +107,9 @@ const debugTest = (0, import_testTool.defineTestTool)({
|
|
|
115
107
|
return { content: [] };
|
|
116
108
|
}
|
|
117
109
|
});
|
|
118
|
-
const setupPage = (0, import_testTool.defineTestTool)({
|
|
119
|
-
schema: {
|
|
120
|
-
name: "test_setup_page",
|
|
121
|
-
title: "Setup page",
|
|
122
|
-
description: "Setup the page for test.",
|
|
123
|
-
inputSchema: import_bundle.z.object({
|
|
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.'),
|
|
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.')
|
|
126
|
-
}),
|
|
127
|
-
type: "readOnly"
|
|
128
|
-
},
|
|
129
|
-
handle: async (context, params, progress) => {
|
|
130
|
-
const { screen } = createScreen(progress);
|
|
131
|
-
const configDir = context.configLocation.configDir;
|
|
132
|
-
const reporter = new import_list.default({ configDir, screen });
|
|
133
|
-
const testRunner = await context.createTestRunner();
|
|
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';
|
|
142
|
-
|
|
143
|
-
test.describe('Test group', () => {
|
|
144
|
-
test('seed', async ({ page }) => {
|
|
145
|
-
// generate code here.
|
|
146
|
-
});
|
|
147
|
-
});
|
|
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(context.rootPath, params.seedFile));
|
|
154
|
-
for (const candidateFile of candidateFiles) {
|
|
155
|
-
if (await (0, import_util.fileExistsAsync)(candidateFile)) {
|
|
156
|
-
seedFile = candidateFile;
|
|
157
|
-
break;
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
if (!seedFile)
|
|
161
|
-
throw new Error("seed test not found.");
|
|
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
|
-
` });
|
|
170
|
-
const result = await testRunner.runTests(reporter, {
|
|
171
|
-
headed: !context.options?.headless,
|
|
172
|
-
locations: ["/" + (0, import_utils.escapeRegExp)(seedFile) + "/"],
|
|
173
|
-
projects: params.project ? [params.project] : void 0,
|
|
174
|
-
timeout: 0,
|
|
175
|
-
workers: 1,
|
|
176
|
-
pauseAtEnd: true,
|
|
177
|
-
disableConfigReporters: true,
|
|
178
|
-
failOnLoadErrors: true
|
|
179
|
-
});
|
|
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: [] };
|
|
185
|
-
}
|
|
186
|
-
});
|
|
187
|
-
function createScreen(progress) {
|
|
188
|
-
const stream = new import_streams.StringWriteStream(progress);
|
|
189
|
-
const screen = {
|
|
190
|
-
...import_base.terminalScreen,
|
|
191
|
-
isTTY: false,
|
|
192
|
-
colors: import_utils.noColors,
|
|
193
|
-
stdout: stream,
|
|
194
|
-
stderr: stream
|
|
195
|
-
};
|
|
196
|
-
return { screen, stream };
|
|
197
|
-
}
|
|
198
110
|
// Annotate the CommonJS export names for ESM import in node:
|
|
199
111
|
0 && (module.exports = {
|
|
200
112
|
debugTest,
|
|
201
113
|
listTests,
|
|
202
|
-
runTests
|
|
203
|
-
setupPage
|
|
114
|
+
runTests
|
|
204
115
|
});
|
package/lib/program.js
CHANGED
|
@@ -48,6 +48,7 @@ var import_testRunner = require("./runner/testRunner");
|
|
|
48
48
|
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
|
+
var import_seed = require("./mcp/test/seed");
|
|
51
52
|
var import_program3 = require("./mcp/program");
|
|
52
53
|
var import_watchdog = require("./mcp/browser/watchdog");
|
|
53
54
|
var import_generateAgents = require("./agents/generateAgents");
|
|
@@ -165,7 +166,13 @@ function addTestMCPServerCommand(program3) {
|
|
|
165
166
|
version: packageJSON.version,
|
|
166
167
|
create: () => new import_testBackend.TestServerBackend(options.config, { muteConsole: options.port === void 0, headless: options.headless })
|
|
167
168
|
};
|
|
168
|
-
const mdbUrl = await (0, import_exports.runMainBackend)(
|
|
169
|
+
const mdbUrl = await (0, import_exports.runMainBackend)(
|
|
170
|
+
backendFactory,
|
|
171
|
+
import_testBackend.TestServerBackend.allowedOnPause,
|
|
172
|
+
{
|
|
173
|
+
port: options.port === void 0 ? void 0 : +options.port
|
|
174
|
+
}
|
|
175
|
+
);
|
|
169
176
|
if (mdbUrl)
|
|
170
177
|
console.error("MCP Listening on: ", mdbUrl);
|
|
171
178
|
});
|
|
@@ -176,15 +183,21 @@ function addInitAgentsCommand(program3) {
|
|
|
176
183
|
const option = command.createOption("--loop <loop>", "Agentic loop provider");
|
|
177
184
|
option.choices(["vscode", "claude", "opencode"]);
|
|
178
185
|
command.addOption(option);
|
|
186
|
+
command.option("-c, --config <file>", `Configuration file to find a project to use for seed test`);
|
|
187
|
+
command.option("--project <project>", "Project to use for seed test");
|
|
179
188
|
command.action(async (opts) => {
|
|
180
|
-
if (opts.loop === "opencode")
|
|
189
|
+
if (opts.loop === "opencode") {
|
|
181
190
|
await (0, import_generateAgents.initOpencodeRepo)();
|
|
182
|
-
else if (opts.loop === "vscode")
|
|
191
|
+
} else if (opts.loop === "vscode") {
|
|
183
192
|
await (0, import_generateAgents.initVSCodeRepo)();
|
|
184
|
-
else if (opts.loop === "claude")
|
|
193
|
+
} else if (opts.loop === "claude") {
|
|
185
194
|
await (0, import_generateAgents.initClaudeCodeRepo)();
|
|
186
|
-
else
|
|
195
|
+
} else {
|
|
187
196
|
command.help();
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
const config = await (0, import_configLoader.loadConfigFromFile)(opts.config);
|
|
200
|
+
await (0, import_seed.ensureSeedTest)(config, opts.project, true);
|
|
188
201
|
});
|
|
189
202
|
}
|
|
190
203
|
async function runTests(args, opts) {
|
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-1759271123000",
|
|
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-1759271123000"
|
|
68
68
|
},
|
|
69
69
|
"optionalDependencies": {
|
|
70
70
|
"fsevents": "2.3.2"
|
package/types/testReporter.d.ts
CHANGED
|
@@ -773,7 +773,7 @@ export interface TestStep {
|
|
|
773
773
|
* - `hook` for hooks initialization and teardown
|
|
774
774
|
* - `pw:api` for Playwright API calls.
|
|
775
775
|
* - `test.step` for test.step API calls.
|
|
776
|
-
* - `test.attach` for
|
|
776
|
+
* - `test.attach` for testInfo.attach API calls.
|
|
777
777
|
*/
|
|
778
778
|
category: string;
|
|
779
779
|
|