playwright 1.56.1 → 1.57.0
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 +3 -3
- package/ThirdPartyNotices.txt +202 -282
- package/lib/agents/copilot-setup-steps.yml +34 -0
- package/lib/agents/generateAgents.js +292 -160
- package/lib/agents/playwright-test-coverage.prompt.md +31 -0
- package/lib/agents/playwright-test-generate.prompt.md +8 -0
- package/lib/agents/{generator.md → playwright-test-generator.agent.md} +8 -22
- package/lib/agents/playwright-test-heal.prompt.md +6 -0
- package/lib/agents/{healer.md → playwright-test-healer.agent.md} +5 -28
- package/lib/agents/playwright-test-plan.prompt.md +9 -0
- package/lib/agents/{planner.md → playwright-test-planner.agent.md} +5 -68
- package/lib/common/config.js +6 -0
- package/lib/common/expectBundle.js +0 -9
- package/lib/common/expectBundleImpl.js +267 -249
- package/lib/common/testLoader.js +3 -2
- package/lib/common/testType.js +3 -12
- package/lib/common/validators.js +68 -0
- package/lib/index.js +9 -9
- package/lib/isomorphic/teleReceiver.js +1 -0
- package/lib/isomorphic/testServerConnection.js +14 -0
- package/lib/loader/loaderMain.js +1 -1
- package/lib/matchers/expect.js +12 -13
- package/lib/matchers/matchers.js +16 -0
- package/lib/mcp/browser/browserServerBackend.js +1 -1
- package/lib/mcp/browser/config.js +9 -24
- package/lib/mcp/browser/context.js +4 -21
- package/lib/mcp/browser/response.js +20 -11
- package/lib/mcp/browser/tab.js +25 -10
- package/lib/mcp/browser/tools/evaluate.js +2 -3
- package/lib/mcp/browser/tools/form.js +2 -3
- package/lib/mcp/browser/tools/keyboard.js +4 -5
- package/lib/mcp/browser/tools/pdf.js +1 -1
- package/lib/mcp/browser/tools/runCode.js +75 -0
- package/lib/mcp/browser/tools/screenshot.js +33 -15
- package/lib/mcp/browser/tools/snapshot.js +13 -14
- package/lib/mcp/browser/tools/tabs.js +2 -2
- package/lib/mcp/browser/tools/utils.js +0 -11
- package/lib/mcp/browser/tools/verify.js +3 -4
- package/lib/mcp/browser/tools.js +2 -0
- package/lib/mcp/program.js +21 -1
- package/lib/mcp/sdk/exports.js +1 -3
- package/lib/mcp/sdk/http.js +9 -2
- package/lib/mcp/sdk/proxyBackend.js +1 -1
- package/lib/mcp/sdk/server.js +13 -5
- package/lib/mcp/sdk/tool.js +2 -6
- package/lib/mcp/test/browserBackend.js +43 -33
- package/lib/mcp/test/generatorTools.js +3 -3
- package/lib/mcp/test/plannerTools.js +103 -5
- package/lib/mcp/test/seed.js +25 -15
- package/lib/mcp/test/streams.js +9 -4
- package/lib/mcp/test/testBackend.js +31 -29
- package/lib/mcp/test/testContext.js +143 -40
- package/lib/mcp/test/testTools.js +12 -21
- package/lib/plugins/webServerPlugin.js +37 -9
- package/lib/program.js +11 -20
- package/lib/reporters/html.js +2 -23
- package/lib/reporters/internalReporter.js +4 -2
- package/lib/reporters/junit.js +4 -2
- package/lib/reporters/list.js +1 -5
- package/lib/reporters/merge.js +12 -6
- package/lib/reporters/teleEmitter.js +3 -1
- package/lib/runner/dispatcher.js +26 -2
- package/lib/runner/failureTracker.js +5 -5
- package/lib/runner/loadUtils.js +2 -1
- package/lib/runner/loaderHost.js +1 -1
- package/lib/runner/reporters.js +5 -4
- package/lib/runner/testRunner.js +8 -9
- package/lib/runner/testServer.js +8 -3
- package/lib/runner/workerHost.js +3 -0
- package/lib/worker/testInfo.js +28 -17
- package/lib/worker/testTracing.js +1 -0
- package/lib/worker/workerMain.js +15 -6
- package/package.json +2 -2
- package/types/test.d.ts +96 -3
- package/types/testReporter.d.ts +5 -0
- package/lib/mcp/sdk/mdb.js +0 -208
|
@@ -29,10 +29,12 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
29
29
|
var testContext_exports = {};
|
|
30
30
|
__export(testContext_exports, {
|
|
31
31
|
GeneratorJournal: () => GeneratorJournal,
|
|
32
|
-
TestContext: () => TestContext
|
|
32
|
+
TestContext: () => TestContext,
|
|
33
|
+
createScreen: () => createScreen
|
|
33
34
|
});
|
|
34
35
|
module.exports = __toCommonJS(testContext_exports);
|
|
35
36
|
var import_fs = __toESM(require("fs"));
|
|
37
|
+
var import_os = __toESM(require("os"));
|
|
36
38
|
var import_path = __toESM(require("path"));
|
|
37
39
|
var import_utils = require("playwright-core/lib/utils");
|
|
38
40
|
var import_base = require("../../reporters/base");
|
|
@@ -41,6 +43,10 @@ var import_streams = require("./streams");
|
|
|
41
43
|
var import_util = require("../../util");
|
|
42
44
|
var import_testRunner = require("../../runner/testRunner");
|
|
43
45
|
var import_seed = require("./seed");
|
|
46
|
+
var import_exports = require("../sdk/exports");
|
|
47
|
+
var import_configLoader = require("../../common/configLoader");
|
|
48
|
+
var import_response = require("../browser/response");
|
|
49
|
+
var import_log = require("../log");
|
|
44
50
|
class GeneratorJournal {
|
|
45
51
|
constructor(rootPath, plan, seed) {
|
|
46
52
|
this._rootPath = rootPath;
|
|
@@ -70,35 +76,55 @@ ${step.code}
|
|
|
70
76
|
}
|
|
71
77
|
}
|
|
72
78
|
class TestContext {
|
|
73
|
-
constructor(options) {
|
|
74
|
-
this.
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
this.
|
|
78
|
-
|
|
79
|
+
constructor(clientInfo, configPath, options) {
|
|
80
|
+
this._clientInfo = clientInfo;
|
|
81
|
+
const rootPath = (0, import_exports.firstRootPath)(clientInfo);
|
|
82
|
+
this._configLocation = (0, import_configLoader.resolveConfigLocation)(configPath || rootPath);
|
|
83
|
+
this.rootPath = rootPath || this._configLocation.configDir;
|
|
84
|
+
if (options?.headless !== void 0)
|
|
85
|
+
this.computedHeaded = !options.headless;
|
|
86
|
+
else
|
|
87
|
+
this.computedHeaded = !process.env.CI && !(import_os.default.platform() === "linux" && !process.env.DISPLAY);
|
|
79
88
|
}
|
|
80
89
|
existingTestRunner() {
|
|
81
|
-
return this.
|
|
90
|
+
return this._testRunnerAndScreen?.testRunner;
|
|
91
|
+
}
|
|
92
|
+
async _cleanupTestRunner() {
|
|
93
|
+
if (!this._testRunnerAndScreen)
|
|
94
|
+
return;
|
|
95
|
+
await this._testRunnerAndScreen.testRunner.stopTests();
|
|
96
|
+
this._testRunnerAndScreen.claimStdio();
|
|
97
|
+
try {
|
|
98
|
+
await this._testRunnerAndScreen.testRunner.runGlobalTeardown();
|
|
99
|
+
} finally {
|
|
100
|
+
this._testRunnerAndScreen.releaseStdio();
|
|
101
|
+
this._testRunnerAndScreen = void 0;
|
|
102
|
+
}
|
|
82
103
|
}
|
|
83
104
|
async createTestRunner() {
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
const testRunner = new import_testRunner.TestRunner(this.configLocation, {});
|
|
105
|
+
await this._cleanupTestRunner();
|
|
106
|
+
const testRunner = new import_testRunner.TestRunner(this._configLocation, {});
|
|
87
107
|
await testRunner.initialize({});
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
108
|
+
const testPaused = new import_utils.ManualPromise();
|
|
109
|
+
const testRunnerAndScreen = {
|
|
110
|
+
...createScreen(),
|
|
111
|
+
testRunner,
|
|
112
|
+
waitForTestPaused: () => testPaused
|
|
113
|
+
};
|
|
114
|
+
this._testRunnerAndScreen = testRunnerAndScreen;
|
|
115
|
+
testRunner.on(import_testRunner.TestRunnerEvent.TestPaused, (params) => {
|
|
116
|
+
testRunnerAndScreen.sendMessageToPausedTest = params.sendMessage;
|
|
117
|
+
testPaused.resolve();
|
|
91
118
|
});
|
|
92
|
-
|
|
93
|
-
return testRunner;
|
|
119
|
+
return testRunnerAndScreen;
|
|
94
120
|
}
|
|
95
121
|
async getOrCreateSeedFile(seedFile, projectName) {
|
|
96
|
-
const configDir = this.
|
|
97
|
-
const testRunner = await this.createTestRunner();
|
|
122
|
+
const configDir = this._configLocation.configDir;
|
|
123
|
+
const { testRunner } = await this.createTestRunner();
|
|
98
124
|
const config = await testRunner.loadConfig();
|
|
99
125
|
const project = (0, import_seed.seedProject)(config, projectName);
|
|
100
126
|
if (!seedFile) {
|
|
101
|
-
seedFile = await (0, import_seed.
|
|
127
|
+
seedFile = await (0, import_seed.ensureSeedFile)(project);
|
|
102
128
|
} else {
|
|
103
129
|
const candidateFiles = [];
|
|
104
130
|
const testDir = project.project.testDir;
|
|
@@ -123,13 +149,9 @@ class TestContext {
|
|
|
123
149
|
projectName: project.project.name
|
|
124
150
|
};
|
|
125
151
|
}
|
|
126
|
-
async runSeedTest(seedFile, projectName
|
|
127
|
-
const
|
|
128
|
-
|
|
129
|
-
const reporter = new import_list.default({ configDir, screen });
|
|
130
|
-
const testRunner = await this.createTestRunner();
|
|
131
|
-
const result = await testRunner.runTests(reporter, {
|
|
132
|
-
headed: !this.options?.headless,
|
|
152
|
+
async runSeedTest(seedFile, projectName) {
|
|
153
|
+
const result = await this.runTestsWithGlobalSetupAndPossiblePause({
|
|
154
|
+
headed: this.computedHeaded,
|
|
133
155
|
locations: ["/" + (0, import_utils.escapeRegExp)(seedFile) + "/"],
|
|
134
156
|
projects: [projectName],
|
|
135
157
|
timeout: 0,
|
|
@@ -138,25 +160,105 @@ class TestContext {
|
|
|
138
160
|
disableConfigReporters: true,
|
|
139
161
|
failOnLoadErrors: true
|
|
140
162
|
});
|
|
141
|
-
if (result.status === "passed"
|
|
142
|
-
|
|
143
|
-
if (result.status !== "
|
|
144
|
-
|
|
163
|
+
if (result.status === "passed")
|
|
164
|
+
result.output += "\nError: seed test not found.";
|
|
165
|
+
else if (result.status !== "paused")
|
|
166
|
+
result.output += "\nError while running the seed test.";
|
|
167
|
+
return result;
|
|
145
168
|
}
|
|
146
|
-
|
|
147
|
-
const
|
|
148
|
-
const
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
169
|
+
async runTestsWithGlobalSetupAndPossiblePause(params) {
|
|
170
|
+
const configDir = this._configLocation.configDir;
|
|
171
|
+
const testRunnerAndScreen = await this.createTestRunner();
|
|
172
|
+
const { testRunner, screen, claimStdio, releaseStdio } = testRunnerAndScreen;
|
|
173
|
+
claimStdio();
|
|
174
|
+
try {
|
|
175
|
+
const setupReporter = new import_list.default({ configDir, screen, includeTestId: true });
|
|
176
|
+
const { status: status2 } = await testRunner.runGlobalSetup([setupReporter]);
|
|
177
|
+
if (status2 !== "passed")
|
|
178
|
+
return { output: testRunnerAndScreen.output.join("\n"), status: status2 };
|
|
179
|
+
} finally {
|
|
180
|
+
releaseStdio();
|
|
181
|
+
}
|
|
182
|
+
let status = "passed";
|
|
183
|
+
const cleanup = async () => {
|
|
184
|
+
claimStdio();
|
|
185
|
+
try {
|
|
186
|
+
const result = await testRunner.runGlobalTeardown();
|
|
187
|
+
if (status === "passed")
|
|
188
|
+
status = result.status;
|
|
189
|
+
} finally {
|
|
190
|
+
releaseStdio();
|
|
191
|
+
}
|
|
154
192
|
};
|
|
155
|
-
|
|
193
|
+
try {
|
|
194
|
+
const reporter = new import_list.default({ configDir, screen, includeTestId: true });
|
|
195
|
+
status = await Promise.race([
|
|
196
|
+
testRunner.runTests(reporter, params).then((result) => result.status),
|
|
197
|
+
testRunnerAndScreen.waitForTestPaused().then(() => "paused")
|
|
198
|
+
]);
|
|
199
|
+
if (status === "paused") {
|
|
200
|
+
const response = await testRunnerAndScreen.sendMessageToPausedTest({ request: { initialize: { clientInfo: this._clientInfo } } });
|
|
201
|
+
if (response.error)
|
|
202
|
+
throw new Error(response.error.message);
|
|
203
|
+
testRunnerAndScreen.output.push(response.response.initialize.pausedMessage);
|
|
204
|
+
return { output: testRunnerAndScreen.output.join("\n"), status };
|
|
205
|
+
}
|
|
206
|
+
} catch (e) {
|
|
207
|
+
status = "failed";
|
|
208
|
+
testRunnerAndScreen.output.push(String(e));
|
|
209
|
+
await cleanup();
|
|
210
|
+
return { output: testRunnerAndScreen.output.join("\n"), status };
|
|
211
|
+
}
|
|
212
|
+
await cleanup();
|
|
213
|
+
return { output: testRunnerAndScreen.output.join("\n"), status };
|
|
156
214
|
}
|
|
157
215
|
async close() {
|
|
216
|
+
await this._cleanupTestRunner().catch(import_log.logUnhandledError);
|
|
217
|
+
}
|
|
218
|
+
async sendMessageToPausedTest(request) {
|
|
219
|
+
const sendMessage = this._testRunnerAndScreen?.sendMessageToPausedTest;
|
|
220
|
+
if (!sendMessage)
|
|
221
|
+
throw new Error("Must setup test before interacting with the page");
|
|
222
|
+
const result = await sendMessage({ request });
|
|
223
|
+
if (result.error)
|
|
224
|
+
throw new Error(result.error.message);
|
|
225
|
+
if (typeof request?.callTool?.arguments?.["intent"] === "string") {
|
|
226
|
+
const response = (0, import_response.parseResponse)(result.response.callTool);
|
|
227
|
+
if (response && !response.isError && response.code)
|
|
228
|
+
this.generatorJournal?.logStep(request.callTool.arguments["intent"], response.code);
|
|
229
|
+
}
|
|
230
|
+
return result.response;
|
|
158
231
|
}
|
|
159
232
|
}
|
|
233
|
+
function createScreen() {
|
|
234
|
+
const output = [];
|
|
235
|
+
const stdout = new import_streams.StringWriteStream(output, "stdout");
|
|
236
|
+
const stderr = new import_streams.StringWriteStream(output, "stderr");
|
|
237
|
+
const screen = {
|
|
238
|
+
...import_base.terminalScreen,
|
|
239
|
+
isTTY: false,
|
|
240
|
+
colors: import_utils.noColors,
|
|
241
|
+
stdout,
|
|
242
|
+
stderr
|
|
243
|
+
};
|
|
244
|
+
const originalStdoutWrite = process.stdout.write;
|
|
245
|
+
const originalStderrWrite = process.stderr.write;
|
|
246
|
+
const claimStdio = () => {
|
|
247
|
+
process.stdout.write = (chunk) => {
|
|
248
|
+
stdout.write(chunk);
|
|
249
|
+
return true;
|
|
250
|
+
};
|
|
251
|
+
process.stderr.write = (chunk) => {
|
|
252
|
+
stderr.write(chunk);
|
|
253
|
+
return true;
|
|
254
|
+
};
|
|
255
|
+
};
|
|
256
|
+
const releaseStdio = () => {
|
|
257
|
+
process.stdout.write = originalStdoutWrite;
|
|
258
|
+
process.stderr.write = originalStderrWrite;
|
|
259
|
+
};
|
|
260
|
+
return { screen, claimStdio, releaseStdio, output };
|
|
261
|
+
}
|
|
160
262
|
const bestPracticesMarkdown = `
|
|
161
263
|
# Best practices
|
|
162
264
|
- Do not improvise, do not add directives that were not asked for
|
|
@@ -172,5 +274,6 @@ const bestPracticesMarkdown = `
|
|
|
172
274
|
// Annotate the CommonJS export names for ESM import in node:
|
|
173
275
|
0 && (module.exports = {
|
|
174
276
|
GeneratorJournal,
|
|
175
|
-
TestContext
|
|
277
|
+
TestContext,
|
|
278
|
+
createScreen
|
|
176
279
|
});
|
|
@@ -34,7 +34,6 @@ __export(testTools_exports, {
|
|
|
34
34
|
});
|
|
35
35
|
module.exports = __toCommonJS(testTools_exports);
|
|
36
36
|
var import_bundle = require("../sdk/bundle");
|
|
37
|
-
var import_list = __toESM(require("../../reporters/list"));
|
|
38
37
|
var import_listModeReporter = __toESM(require("../../reporters/listModeReporter"));
|
|
39
38
|
var import_testTool = require("./testTool");
|
|
40
39
|
const listTests = (0, import_testTool.defineTestTool)({
|
|
@@ -45,12 +44,11 @@ const listTests = (0, import_testTool.defineTestTool)({
|
|
|
45
44
|
inputSchema: import_bundle.z.object({}),
|
|
46
45
|
type: "readOnly"
|
|
47
46
|
},
|
|
48
|
-
handle: async (context
|
|
49
|
-
const { screen } = context.
|
|
47
|
+
handle: async (context) => {
|
|
48
|
+
const { testRunner, screen, output } = await context.createTestRunner();
|
|
50
49
|
const reporter = new import_listModeReporter.default({ screen, includeTestId: true });
|
|
51
|
-
const testRunner = await context.createTestRunner();
|
|
52
50
|
await testRunner.listTests(reporter, {});
|
|
53
|
-
return { content:
|
|
51
|
+
return { content: output.map((text) => ({ type: "text", text })) };
|
|
54
52
|
}
|
|
55
53
|
});
|
|
56
54
|
const runTests = (0, import_testTool.defineTestTool)({
|
|
@@ -64,17 +62,13 @@ const runTests = (0, import_testTool.defineTestTool)({
|
|
|
64
62
|
}),
|
|
65
63
|
type: "readOnly"
|
|
66
64
|
},
|
|
67
|
-
handle: async (context, params
|
|
68
|
-
const {
|
|
69
|
-
const configDir = context.configLocation.configDir;
|
|
70
|
-
const reporter = new import_list.default({ configDir, screen, includeTestId: true, prefixStdio: "out" });
|
|
71
|
-
const testRunner = await context.createTestRunner();
|
|
72
|
-
await testRunner.runTests(reporter, {
|
|
65
|
+
handle: async (context, params) => {
|
|
66
|
+
const { output } = await context.runTestsWithGlobalSetupAndPossiblePause({
|
|
73
67
|
locations: params.locations,
|
|
74
68
|
projects: params.projects,
|
|
75
69
|
disableConfigReporters: true
|
|
76
70
|
});
|
|
77
|
-
return { content: [] };
|
|
71
|
+
return { content: [{ type: "text", text: output }] };
|
|
78
72
|
}
|
|
79
73
|
});
|
|
80
74
|
const debugTest = (0, import_testTool.defineTestTool)({
|
|
@@ -90,21 +84,18 @@ const debugTest = (0, import_testTool.defineTestTool)({
|
|
|
90
84
|
}),
|
|
91
85
|
type: "readOnly"
|
|
92
86
|
},
|
|
93
|
-
handle: async (context, params
|
|
94
|
-
const {
|
|
95
|
-
|
|
96
|
-
const reporter = new import_list.default({ configDir, screen, includeTestId: true, prefixStdio: "out" });
|
|
97
|
-
const testRunner = await context.createTestRunner();
|
|
98
|
-
await testRunner.runTests(reporter, {
|
|
99
|
-
headed: !context.options?.headless,
|
|
87
|
+
handle: async (context, params) => {
|
|
88
|
+
const { output, status } = await context.runTestsWithGlobalSetupAndPossiblePause({
|
|
89
|
+
headed: context.computedHeaded,
|
|
100
90
|
testIds: [params.test.id],
|
|
101
91
|
// For automatic recovery
|
|
102
92
|
timeout: 0,
|
|
103
93
|
workers: 1,
|
|
104
94
|
pauseOnError: true,
|
|
105
|
-
disableConfigReporters: true
|
|
95
|
+
disableConfigReporters: true,
|
|
96
|
+
actionTimeout: 5e3
|
|
106
97
|
});
|
|
107
|
-
return { content: [] };
|
|
98
|
+
return { content: [{ type: "text", text: output }], isError: status !== "paused" && status !== "passed" };
|
|
108
99
|
}
|
|
109
100
|
});
|
|
110
101
|
// Annotate the CommonJS export names for ESM import in node:
|
|
@@ -115,17 +115,42 @@ class WebServerPlugin {
|
|
|
115
115
|
});
|
|
116
116
|
this._killProcess = gracefullyClose;
|
|
117
117
|
debugWebServer(`Process started`);
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
118
|
+
if (this._options.wait?.stdout || this._options.wait?.stderr)
|
|
119
|
+
this._waitForStdioPromise = new import_utils.ManualPromise();
|
|
120
|
+
const stdioWaitCollectors = {
|
|
121
|
+
stdout: this._options.wait?.stdout ? "" : void 0,
|
|
122
|
+
stderr: this._options.wait?.stderr ? "" : void 0
|
|
123
|
+
};
|
|
122
124
|
launchedProcess.stdout.on("data", (data) => {
|
|
123
125
|
if (debugWebServer.enabled || this._options.stdout === "pipe")
|
|
124
126
|
this._reporter.onStdOut?.(prefixOutputLines(data.toString(), this._options.name));
|
|
125
127
|
});
|
|
128
|
+
launchedProcess.stderr.on("data", (data) => {
|
|
129
|
+
if (debugWebServer.enabled || (this._options.stderr === "pipe" || !this._options.stderr))
|
|
130
|
+
this._reporter.onStdErr?.(prefixOutputLines(data.toString(), this._options.name));
|
|
131
|
+
});
|
|
132
|
+
const resolveStdioPromise = () => {
|
|
133
|
+
stdioWaitCollectors.stdout = void 0;
|
|
134
|
+
stdioWaitCollectors.stderr = void 0;
|
|
135
|
+
this._waitForStdioPromise?.resolve();
|
|
136
|
+
};
|
|
137
|
+
for (const stdio of ["stdout", "stderr"]) {
|
|
138
|
+
launchedProcess[stdio].on("data", (data) => {
|
|
139
|
+
if (!this._options.wait?.[stdio] || stdioWaitCollectors[stdio] === void 0)
|
|
140
|
+
return;
|
|
141
|
+
stdioWaitCollectors[stdio] += data.toString();
|
|
142
|
+
this._options.wait[stdio].lastIndex = 0;
|
|
143
|
+
const result = this._options.wait[stdio].exec(stdioWaitCollectors[stdio]);
|
|
144
|
+
if (result) {
|
|
145
|
+
for (const [key, value] of Object.entries(result.groups || {}))
|
|
146
|
+
process.env[key.toUpperCase()] = value;
|
|
147
|
+
resolveStdioPromise();
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
}
|
|
126
151
|
}
|
|
127
152
|
async _waitForProcess() {
|
|
128
|
-
if (!this._isAvailableCallback) {
|
|
153
|
+
if (!this._isAvailableCallback && !this._waitForStdioPromise) {
|
|
129
154
|
this._processExitedPromise.catch(() => {
|
|
130
155
|
});
|
|
131
156
|
return;
|
|
@@ -133,10 +158,13 @@ class WebServerPlugin {
|
|
|
133
158
|
debugWebServer(`Waiting for availability...`);
|
|
134
159
|
const launchTimeout = this._options.timeout || 60 * 1e3;
|
|
135
160
|
const cancellationToken = { canceled: false };
|
|
136
|
-
const
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
161
|
+
const deadline = (0, import_utils.monotonicTime)() + launchTimeout;
|
|
162
|
+
const racingPromises = [this._processExitedPromise];
|
|
163
|
+
if (this._isAvailableCallback)
|
|
164
|
+
racingPromises.push((0, import_utils.raceAgainstDeadline)(() => waitFor(this._isAvailableCallback, cancellationToken), deadline));
|
|
165
|
+
if (this._waitForStdioPromise)
|
|
166
|
+
racingPromises.push((0, import_utils.raceAgainstDeadline)(() => this._waitForStdioPromise, deadline));
|
|
167
|
+
const { timedOut } = await Promise.race(racingPromises);
|
|
140
168
|
cancellationToken.canceled = true;
|
|
141
169
|
if (timedOut)
|
|
142
170
|
throw new Error(`Timed out waiting ${launchTimeout}ms from config.webServer.`);
|
package/lib/program.js
CHANGED
|
@@ -46,9 +46,8 @@ var testServer = __toESM(require("./runner/testServer"));
|
|
|
46
46
|
var import_watchMode = require("./runner/watchMode");
|
|
47
47
|
var import_testRunner = require("./runner/testRunner");
|
|
48
48
|
var import_reporters = require("./runner/reporters");
|
|
49
|
-
var
|
|
49
|
+
var mcp = __toESM(require("./mcp/sdk/exports"));
|
|
50
50
|
var import_testBackend = require("./mcp/test/testBackend");
|
|
51
|
-
var import_seed = require("./mcp/test/seed");
|
|
52
51
|
var import_program3 = require("./mcp/program");
|
|
53
52
|
var import_watchdog = require("./mcp/browser/watchdog");
|
|
54
53
|
var import_generateAgents = require("./agents/generateAgents");
|
|
@@ -160,44 +159,36 @@ function addTestMCPServerCommand(program3) {
|
|
|
160
159
|
command.option("--port <port>", "port to listen on for SSE transport.");
|
|
161
160
|
command.action(async (options) => {
|
|
162
161
|
(0, import_watchdog.setupExitWatchdog)();
|
|
163
|
-
const
|
|
162
|
+
const factory = {
|
|
164
163
|
name: "Playwright Test Runner",
|
|
165
164
|
nameInConfig: "playwright-test-runner",
|
|
166
165
|
version: packageJSON.version,
|
|
167
166
|
create: () => new import_testBackend.TestServerBackend(options.config, { muteConsole: options.port === void 0, headless: options.headless })
|
|
168
167
|
};
|
|
169
|
-
|
|
170
|
-
backendFactory,
|
|
171
|
-
{
|
|
172
|
-
port: options.port === void 0 ? void 0 : +options.port
|
|
173
|
-
}
|
|
174
|
-
);
|
|
175
|
-
if (mdbUrl)
|
|
176
|
-
console.error("MCP Listening on: ", mdbUrl);
|
|
168
|
+
await mcp.start(factory, { port: options.port === void 0 ? void 0 : +options.port, host: options.host });
|
|
177
169
|
});
|
|
178
170
|
}
|
|
179
171
|
function addInitAgentsCommand(program3) {
|
|
180
172
|
const command = program3.command("init-agents");
|
|
181
173
|
command.description("Initialize repository agents");
|
|
182
174
|
const option = command.createOption("--loop <loop>", "Agentic loop provider");
|
|
183
|
-
option.choices(["
|
|
175
|
+
option.choices(["claude", "copilot", "opencode", "vscode", "vscode-legacy"]);
|
|
184
176
|
command.addOption(option);
|
|
185
177
|
command.option("-c, --config <file>", `Configuration file to find a project to use for seed test`);
|
|
186
178
|
command.option("--project <project>", "Project to use for seed test");
|
|
179
|
+
command.option("--prompts", "Whether to include prompts in the agent initialization");
|
|
187
180
|
command.action(async (opts) => {
|
|
181
|
+
const config = await (0, import_configLoader.loadConfigFromFile)(opts.config);
|
|
188
182
|
if (opts.loop === "opencode") {
|
|
189
|
-
await (
|
|
190
|
-
} else if (opts.loop === "vscode") {
|
|
191
|
-
await (
|
|
183
|
+
await import_generateAgents.OpencodeGenerator.init(config, opts.project, opts.prompts);
|
|
184
|
+
} else if (opts.loop === "vscode-legacy") {
|
|
185
|
+
await import_generateAgents.VSCodeGenerator.init(config, opts.project);
|
|
192
186
|
} else if (opts.loop === "claude") {
|
|
193
|
-
await (
|
|
187
|
+
await import_generateAgents.ClaudeGenerator.init(config, opts.project, opts.prompts);
|
|
194
188
|
} else {
|
|
195
|
-
|
|
189
|
+
await import_generateAgents.CopilotGenerator.init(config, opts.project, opts.prompts);
|
|
196
190
|
return;
|
|
197
191
|
}
|
|
198
|
-
const config = await (0, import_configLoader.loadConfigFromFile)(opts.config);
|
|
199
|
-
const project = (0, import_seed.seedProject)(config, opts.project);
|
|
200
|
-
await (0, import_seed.ensureSeedTest)(project, true);
|
|
201
192
|
});
|
|
202
193
|
}
|
|
203
194
|
async function runTests(args, opts) {
|
package/lib/reporters/html.js
CHANGED
|
@@ -130,10 +130,10 @@ class HtmlReporter {
|
|
|
130
130
|
if (process.env.CI || !this._buildResult)
|
|
131
131
|
return;
|
|
132
132
|
const { ok, singleTestId } = this._buildResult;
|
|
133
|
-
const shouldOpen =
|
|
133
|
+
const shouldOpen = !!process.stdin.isTTY && (this._open === "always" || !ok && this._open === "on-failure");
|
|
134
134
|
if (shouldOpen) {
|
|
135
135
|
await showHTMLReport(this._outputFolder, this._host, this._port, singleTestId);
|
|
136
|
-
} else if (this._options._mode === "test" &&
|
|
136
|
+
} else if (this._options._mode === "test" && !!process.stdin.isTTY) {
|
|
137
137
|
const packageManagerCommand = (0, import_utils.getPackageManagerExecCommand)();
|
|
138
138
|
const relativeReportPath = this._outputFolder === standaloneDefaultFolder() ? "" : " " + import_path.default.relative(process.cwd(), this._outputFolder);
|
|
139
139
|
const hostArg = this._host ? ` --host ${this._host}` : "";
|
|
@@ -197,8 +197,6 @@ function startHtmlReportServer(folder) {
|
|
|
197
197
|
return false;
|
|
198
198
|
}
|
|
199
199
|
}
|
|
200
|
-
if (relativePath.endsWith("/stall.js"))
|
|
201
|
-
return true;
|
|
202
200
|
if (relativePath === "/")
|
|
203
201
|
relativePath = "/index.html";
|
|
204
202
|
const absolutePath = import_path.default.join(folder, ...relativePath.split("/"));
|
|
@@ -274,25 +272,6 @@ class HtmlBuilder {
|
|
|
274
272
|
const testFile = data.values().next().value.testFile;
|
|
275
273
|
singleTestId = testFile.tests[0].testId;
|
|
276
274
|
}
|
|
277
|
-
if (process.env.PW_HMR === "1") {
|
|
278
|
-
const redirectFile = import_path.default.join(this._reportFolder, "index.html");
|
|
279
|
-
await this._writeReportData(redirectFile);
|
|
280
|
-
async function redirect() {
|
|
281
|
-
const hmrURL = new URL("http://localhost:44224");
|
|
282
|
-
const popup = window.open(hmrURL);
|
|
283
|
-
const listener = (evt) => {
|
|
284
|
-
if (evt.source === popup && evt.data === "ready") {
|
|
285
|
-
const element = document.getElementById("playwrightReportBase64");
|
|
286
|
-
popup.postMessage(element?.textContent ?? "", hmrURL.origin);
|
|
287
|
-
window.removeEventListener("message", listener);
|
|
288
|
-
window.close();
|
|
289
|
-
}
|
|
290
|
-
};
|
|
291
|
-
window.addEventListener("message", listener);
|
|
292
|
-
}
|
|
293
|
-
import_fs.default.appendFileSync(redirectFile, `<script>(${redirect.toString()})()</script>`);
|
|
294
|
-
return { ok, singleTestId };
|
|
295
|
-
}
|
|
296
275
|
const appFolder = import_path.default.join(require.resolve("playwright-core"), "..", "lib", "vite", "htmlReport");
|
|
297
276
|
await (0, import_utils.copyFileAndMakeWritable)(import_path.default.join(appFolder, "index.html"), import_path.default.join(this._reportFolder, "index.html"));
|
|
298
277
|
if (this._hasTraces) {
|
|
@@ -28,7 +28,8 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
28
28
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
29
|
var internalReporter_exports = {};
|
|
30
30
|
__export(internalReporter_exports, {
|
|
31
|
-
InternalReporter: () => InternalReporter
|
|
31
|
+
InternalReporter: () => InternalReporter,
|
|
32
|
+
addLocationAndSnippetToError: () => addLocationAndSnippetToError
|
|
32
33
|
});
|
|
33
34
|
module.exports = __toCommonJS(internalReporter_exports);
|
|
34
35
|
var import_fs = __toESM(require("fs"));
|
|
@@ -126,5 +127,6 @@ function addLocationAndSnippetToError(config, error, file) {
|
|
|
126
127
|
}
|
|
127
128
|
// Annotate the CommonJS export names for ESM import in node:
|
|
128
129
|
0 && (module.exports = {
|
|
129
|
-
InternalReporter
|
|
130
|
+
InternalReporter,
|
|
131
|
+
addLocationAndSnippetToError
|
|
130
132
|
});
|
package/lib/reporters/junit.js
CHANGED
|
@@ -173,8 +173,10 @@ class JUnitReporter {
|
|
|
173
173
|
const systemOut = [];
|
|
174
174
|
const systemErr = [];
|
|
175
175
|
for (const result of test.results) {
|
|
176
|
-
|
|
177
|
-
|
|
176
|
+
for (const item of result.stdout)
|
|
177
|
+
systemOut.push(item.toString());
|
|
178
|
+
for (const item of result.stderr)
|
|
179
|
+
systemErr.push(item.toString());
|
|
178
180
|
for (const attachment of result.attachments) {
|
|
179
181
|
if (!attachment.path)
|
|
180
182
|
continue;
|
package/lib/reporters/list.js
CHANGED
|
@@ -39,7 +39,6 @@ class ListReporter extends import_base.TerminalReporter {
|
|
|
39
39
|
this._stepIndex = /* @__PURE__ */ new Map();
|
|
40
40
|
this._needNewLine = false;
|
|
41
41
|
this._printSteps = (0, import_utils.getAsBooleanFromENV)("PLAYWRIGHT_LIST_PRINT_STEPS", options?.printSteps);
|
|
42
|
-
this._prefixStdio = options?.prefixStdio;
|
|
43
42
|
}
|
|
44
43
|
onBegin(suite) {
|
|
45
44
|
super.onBegin(suite);
|
|
@@ -143,10 +142,7 @@ class ListReporter extends import_base.TerminalReporter {
|
|
|
143
142
|
return;
|
|
144
143
|
const text = chunk.toString("utf-8");
|
|
145
144
|
this._updateLineCountAndNewLineFlagForOutput(text);
|
|
146
|
-
|
|
147
|
-
stream.write(`[${stdio}] ${chunk}`);
|
|
148
|
-
else
|
|
149
|
-
stream.write(chunk);
|
|
145
|
+
stream.write(chunk);
|
|
150
146
|
}
|
|
151
147
|
onTestEnd(test, result) {
|
|
152
148
|
super.onTestEnd(test, result);
|
package/lib/reporters/merge.js
CHANGED
|
@@ -41,7 +41,7 @@ var import_teleReceiver = require("../isomorphic/teleReceiver");
|
|
|
41
41
|
var import_reporters = require("../runner/reporters");
|
|
42
42
|
var import_util = require("../util");
|
|
43
43
|
async function createMergedReport(config, dir, reporterDescriptions, rootDirOverride) {
|
|
44
|
-
const reporters = await (0, import_reporters.createReporters)(config, "merge",
|
|
44
|
+
const reporters = await (0, import_reporters.createReporters)(config, "merge", reporterDescriptions);
|
|
45
45
|
const multiplexer = new import_multiplexer.Multiplexer(reporters);
|
|
46
46
|
const stringPool = new import_stringInternPool.StringInternPool();
|
|
47
47
|
let printStatus = () => {
|
|
@@ -72,13 +72,15 @@ async function createMergedReport(config, dir, reporterDescriptions, rootDirOver
|
|
|
72
72
|
}
|
|
73
73
|
};
|
|
74
74
|
await dispatchEvents(eventData.prologue);
|
|
75
|
-
for (const { reportFile, eventPatchers, metadata } of eventData.reports) {
|
|
75
|
+
for (const { reportFile, eventPatchers, metadata, tags } of eventData.reports) {
|
|
76
76
|
const reportJsonl = await import_fs.default.promises.readFile(reportFile);
|
|
77
77
|
const events = parseTestEvents(reportJsonl);
|
|
78
78
|
new import_stringInternPool.JsonStringInternalizer(stringPool).traverse(events);
|
|
79
79
|
eventPatchers.patchers.push(new AttachmentPathPatcher(dir));
|
|
80
80
|
if (metadata.name)
|
|
81
81
|
eventPatchers.patchers.push(new GlobalErrorPatcher(metadata.name));
|
|
82
|
+
if (tags.length)
|
|
83
|
+
eventPatchers.patchers.push(new GlobalErrorPatcher(tags.join(" ")));
|
|
82
84
|
eventPatchers.patchEvents(events);
|
|
83
85
|
await dispatchEvents(events);
|
|
84
86
|
}
|
|
@@ -178,18 +180,22 @@ async function mergeEvents(dir, shardReportFiles, stringPool, printStatus, rootD
|
|
|
178
180
|
if (rootDirOverride)
|
|
179
181
|
eventPatchers.patchers.push(new PathSeparatorPatcher(metadata.pathSeparator));
|
|
180
182
|
eventPatchers.patchEvents(parsedEvents);
|
|
183
|
+
let tags = [];
|
|
181
184
|
for (const event of parsedEvents) {
|
|
182
|
-
if (event.method === "onConfigure")
|
|
185
|
+
if (event.method === "onConfigure") {
|
|
183
186
|
configureEvents.push(event);
|
|
184
|
-
|
|
187
|
+
tags = event.params.config.tags || [];
|
|
188
|
+
} else if (event.method === "onProject") {
|
|
185
189
|
projectEvents.push(event);
|
|
186
|
-
else if (event.method === "onEnd")
|
|
190
|
+
} else if (event.method === "onEnd") {
|
|
187
191
|
endEvents.push(event);
|
|
192
|
+
}
|
|
188
193
|
}
|
|
189
194
|
reports.push({
|
|
190
195
|
eventPatchers,
|
|
191
196
|
reportFile: localPath,
|
|
192
|
-
metadata
|
|
197
|
+
metadata,
|
|
198
|
+
tags
|
|
193
199
|
});
|
|
194
200
|
}
|
|
195
201
|
return {
|
|
@@ -154,7 +154,9 @@ class TeleReporterEmitter {
|
|
|
154
154
|
version: config.version,
|
|
155
155
|
workers: config.workers,
|
|
156
156
|
globalSetup: config.globalSetup,
|
|
157
|
-
globalTeardown: config.globalTeardown
|
|
157
|
+
globalTeardown: config.globalTeardown,
|
|
158
|
+
tags: config.tags,
|
|
159
|
+
webServer: config.webServer
|
|
158
160
|
};
|
|
159
161
|
}
|
|
160
162
|
_serializeProject(suite) {
|