playwright 1.55.1 → 1.56.1
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 +3 -3
- package/lib/agents/generateAgents.js +263 -0
- package/lib/agents/generator.md +102 -0
- package/lib/agents/healer.md +78 -0
- package/lib/agents/planner.md +135 -0
- package/lib/common/config.js +1 -1
- package/lib/common/expectBundle.js +3 -0
- package/lib/common/expectBundleImpl.js +51 -51
- package/lib/index.js +7 -8
- package/lib/isomorphic/testServerConnection.js +0 -7
- package/lib/isomorphic/testTree.js +35 -8
- package/lib/matchers/expect.js +8 -21
- package/lib/matchers/matcherHint.js +42 -18
- package/lib/matchers/matchers.js +12 -6
- package/lib/matchers/toBeTruthy.js +16 -14
- package/lib/matchers/toEqual.js +18 -13
- package/lib/matchers/toHaveURL.js +12 -27
- package/lib/matchers/toMatchAriaSnapshot.js +26 -30
- package/lib/matchers/toMatchSnapshot.js +15 -12
- package/lib/matchers/toMatchText.js +29 -35
- package/lib/mcp/browser/actions.d.js +16 -0
- package/lib/mcp/browser/browserContextFactory.js +296 -0
- package/lib/mcp/browser/browserServerBackend.js +76 -0
- package/lib/mcp/browser/codegen.js +66 -0
- package/lib/mcp/browser/config.js +383 -0
- package/lib/mcp/browser/context.js +284 -0
- package/lib/mcp/browser/response.js +228 -0
- package/lib/mcp/browser/sessionLog.js +160 -0
- package/lib/mcp/browser/tab.js +277 -0
- package/lib/mcp/browser/tools/common.js +63 -0
- package/lib/mcp/browser/tools/console.js +44 -0
- package/lib/mcp/browser/tools/dialogs.js +60 -0
- package/lib/mcp/browser/tools/evaluate.js +70 -0
- package/lib/mcp/browser/tools/files.js +58 -0
- package/lib/mcp/browser/tools/form.js +74 -0
- package/lib/mcp/browser/tools/install.js +69 -0
- package/lib/mcp/browser/tools/keyboard.js +85 -0
- package/lib/mcp/browser/tools/mouse.js +107 -0
- package/lib/mcp/browser/tools/navigate.js +62 -0
- package/lib/mcp/browser/tools/network.js +54 -0
- package/lib/mcp/browser/tools/pdf.js +59 -0
- package/lib/mcp/browser/tools/screenshot.js +88 -0
- package/lib/mcp/browser/tools/snapshot.js +182 -0
- package/lib/mcp/browser/tools/tabs.js +67 -0
- package/lib/mcp/browser/tools/tool.js +49 -0
- package/lib/mcp/browser/tools/tracing.js +74 -0
- package/lib/mcp/browser/tools/utils.js +100 -0
- package/lib/mcp/browser/tools/verify.js +154 -0
- package/lib/mcp/browser/tools/wait.js +63 -0
- package/lib/mcp/browser/tools.js +80 -0
- package/lib/mcp/browser/watchdog.js +44 -0
- package/lib/mcp/config.d.js +16 -0
- package/lib/mcp/extension/cdpRelay.js +351 -0
- package/lib/mcp/extension/extensionContextFactory.js +75 -0
- package/lib/mcp/extension/protocol.js +28 -0
- package/lib/mcp/index.js +61 -0
- package/lib/mcp/{tool.js → log.js} +12 -18
- package/lib/mcp/program.js +96 -0
- package/lib/mcp/{bundle.js → sdk/bundle.js} +24 -2
- package/lib/mcp/{exports.js → sdk/exports.js} +12 -10
- package/lib/mcp/{transport.js → sdk/http.js} +79 -60
- package/lib/mcp/sdk/mdb.js +208 -0
- package/lib/mcp/{proxyBackend.js → sdk/proxyBackend.js} +18 -13
- package/lib/mcp/sdk/server.js +190 -0
- package/lib/mcp/sdk/tool.js +51 -0
- package/lib/mcp/test/browserBackend.js +98 -0
- package/lib/mcp/test/generatorTools.js +122 -0
- package/lib/mcp/test/plannerTools.js +46 -0
- package/lib/mcp/test/seed.js +72 -0
- package/lib/mcp/test/streams.js +39 -0
- package/lib/mcp/test/testBackend.js +97 -0
- package/lib/mcp/test/testContext.js +176 -0
- package/lib/mcp/test/testTool.js +30 -0
- package/lib/mcp/test/testTools.js +115 -0
- package/lib/mcpBundleImpl.js +14 -67
- package/lib/plugins/webServerPlugin.js +2 -0
- package/lib/program.js +68 -0
- package/lib/reporters/base.js +15 -17
- package/lib/reporters/html.js +39 -26
- package/lib/reporters/list.js +8 -4
- package/lib/reporters/listModeReporter.js +6 -3
- package/lib/reporters/merge.js +3 -1
- package/lib/reporters/teleEmitter.js +3 -1
- package/lib/runner/dispatcher.js +9 -23
- package/lib/runner/failureTracker.js +12 -16
- package/lib/runner/loadUtils.js +39 -3
- package/lib/runner/projectUtils.js +8 -2
- package/lib/runner/tasks.js +18 -7
- package/lib/runner/testRunner.js +16 -28
- package/lib/runner/testServer.js +17 -23
- package/lib/runner/watchMode.js +1 -53
- package/lib/runner/workerHost.js +8 -10
- package/lib/transform/babelBundleImpl.js +10 -10
- package/lib/transform/compilationCache.js +22 -5
- package/lib/util.js +12 -16
- package/lib/utilsBundleImpl.js +1 -1
- package/lib/worker/fixtureRunner.js +15 -7
- package/lib/worker/testInfo.js +9 -24
- package/lib/worker/workerMain.js +12 -8
- package/package.json +7 -3
- package/types/test.d.ts +17 -8
- package/types/testReporter.d.ts +1 -1
- package/lib/mcp/server.js +0 -118
- /package/lib/mcp/{inProcessTransport.js → sdk/inProcessTransport.js} +0 -0
|
@@ -80,6 +80,8 @@ class WebServerPlugin {
|
|
|
80
80
|
const port = new URL(this._options.url).port;
|
|
81
81
|
throw new Error(`${this._options.url ?? `http://localhost${port ? ":" + port : ""}`} is already used, make sure that nothing is running on the port/url or set reuseExistingServer:true in config.webServer.`);
|
|
82
82
|
}
|
|
83
|
+
if (!this._options.command)
|
|
84
|
+
throw new Error("config.webServer.command cannot be empty");
|
|
83
85
|
debugWebServer(`Starting WebServer process ${this._options.command}...`);
|
|
84
86
|
const { launchedProcess, gracefullyClose } = await (0, import_utils.launchProcess)({
|
|
85
87
|
command: this._options.command,
|
package/lib/program.js
CHANGED
|
@@ -46,6 +46,13 @@ 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 import_exports = require("./mcp/sdk/exports");
|
|
50
|
+
var import_testBackend = require("./mcp/test/testBackend");
|
|
51
|
+
var import_seed = require("./mcp/test/seed");
|
|
52
|
+
var import_program3 = require("./mcp/program");
|
|
53
|
+
var import_watchdog = require("./mcp/browser/watchdog");
|
|
54
|
+
var import_generateAgents = require("./agents/generateAgents");
|
|
55
|
+
const packageJSON = require("../package.json");
|
|
49
56
|
function addTestCommand(program3) {
|
|
50
57
|
const command = program3.command("test [test-filter...]");
|
|
51
58
|
command.description("run tests with Playwright Test");
|
|
@@ -139,6 +146,60 @@ Arguments [dir]:
|
|
|
139
146
|
Examples:
|
|
140
147
|
$ npx playwright merge-reports playwright-report`);
|
|
141
148
|
}
|
|
149
|
+
function addBrowserMCPServerCommand(program3) {
|
|
150
|
+
const command = program3.command("run-mcp-server", { hidden: true });
|
|
151
|
+
command.description("Interact with the browser over MCP");
|
|
152
|
+
(0, import_program3.decorateCommand)(command, packageJSON.version);
|
|
153
|
+
}
|
|
154
|
+
function addTestMCPServerCommand(program3) {
|
|
155
|
+
const command = program3.command("run-test-mcp-server", { hidden: true });
|
|
156
|
+
command.description("Interact with the test runner over MCP");
|
|
157
|
+
command.option("--headless", "run browser in headless mode, headed by default");
|
|
158
|
+
command.option("-c, --config <file>", `Configuration file, or a test directory with optional "playwright.config.{m,c}?{js,ts}"`);
|
|
159
|
+
command.option("--host <host>", "host to bind server to. Default is localhost. Use 0.0.0.0 to bind to all interfaces.");
|
|
160
|
+
command.option("--port <port>", "port to listen on for SSE transport.");
|
|
161
|
+
command.action(async (options) => {
|
|
162
|
+
(0, import_watchdog.setupExitWatchdog)();
|
|
163
|
+
const backendFactory = {
|
|
164
|
+
name: "Playwright Test Runner",
|
|
165
|
+
nameInConfig: "playwright-test-runner",
|
|
166
|
+
version: packageJSON.version,
|
|
167
|
+
create: () => new import_testBackend.TestServerBackend(options.config, { muteConsole: options.port === void 0, headless: options.headless })
|
|
168
|
+
};
|
|
169
|
+
const mdbUrl = await (0, import_exports.runMainBackend)(
|
|
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);
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
function addInitAgentsCommand(program3) {
|
|
180
|
+
const command = program3.command("init-agents");
|
|
181
|
+
command.description("Initialize repository agents");
|
|
182
|
+
const option = command.createOption("--loop <loop>", "Agentic loop provider");
|
|
183
|
+
option.choices(["vscode", "claude", "opencode"]);
|
|
184
|
+
command.addOption(option);
|
|
185
|
+
command.option("-c, --config <file>", `Configuration file to find a project to use for seed test`);
|
|
186
|
+
command.option("--project <project>", "Project to use for seed test");
|
|
187
|
+
command.action(async (opts) => {
|
|
188
|
+
if (opts.loop === "opencode") {
|
|
189
|
+
await (0, import_generateAgents.initOpencodeRepo)();
|
|
190
|
+
} else if (opts.loop === "vscode") {
|
|
191
|
+
await (0, import_generateAgents.initVSCodeRepo)();
|
|
192
|
+
} else if (opts.loop === "claude") {
|
|
193
|
+
await (0, import_generateAgents.initClaudeCodeRepo)();
|
|
194
|
+
} else {
|
|
195
|
+
command.help();
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
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
|
+
});
|
|
202
|
+
}
|
|
142
203
|
async function runTests(args, opts) {
|
|
143
204
|
await (0, import_utils.startProfiling)();
|
|
144
205
|
const cliOverrides = overridesFromOptions(opts);
|
|
@@ -151,6 +212,8 @@ async function runTests(args, opts) {
|
|
|
151
212
|
config.cliProjectFilter = opts.project || void 0;
|
|
152
213
|
config.cliPassWithNoTests = !!opts.passWithNoTests;
|
|
153
214
|
config.cliLastFailed = !!opts.lastFailed;
|
|
215
|
+
config.cliTestList = opts.testList ? import_path.default.resolve(process.cwd(), opts.testList) : void 0;
|
|
216
|
+
config.cliTestListInvert = opts.testListInvert ? import_path.default.resolve(process.cwd(), opts.testListInvert) : void 0;
|
|
154
217
|
(0, import_projectUtils.filterProjects)(config.projects, config.cliProjectFilter);
|
|
155
218
|
if (opts.ui || opts.uiHost || opts.uiPort) {
|
|
156
219
|
if (opts.onlyChanged)
|
|
@@ -321,6 +384,8 @@ const testOptions = [
|
|
|
321
384
|
["--reporter <reporter>", { description: `Reporter to use, comma-separated, can be ${import_config.builtInReporters.map((name) => `"${name}"`).join(", ")} (default: "${import_config.defaultReporter}")` }],
|
|
322
385
|
["--retries <retries>", { description: `Maximum retry count for flaky tests, zero for no retries (default: no retries)` }],
|
|
323
386
|
["--shard <shard>", { description: `Shard tests and execute only the selected shard, specify in the form "current/all", 1-based, for example "3/5"` }],
|
|
387
|
+
["--test-list <file>", { description: `Path to a file containing a list of tests to run. See https://playwright.dev/docs/test-cli for more details.` }],
|
|
388
|
+
["--test-list-invert <file>", { description: `Path to a file containing a list of tests to skip. See https://playwright.dev/docs/test-cli for more details.` }],
|
|
324
389
|
["--timeout <timeout>", { description: `Specify test timeout threshold in milliseconds, zero for unlimited (default: ${import_config.defaultTimeout})` }],
|
|
325
390
|
["--trace <mode>", { description: `Force tracing mode`, choices: kTraceModes }],
|
|
326
391
|
["--tsconfig <path>", { description: `Path to a single tsconfig applicable to all imported files (default: look up tsconfig for each imported file separately)` }],
|
|
@@ -336,8 +401,11 @@ addTestCommand(import_program.program);
|
|
|
336
401
|
addShowReportCommand(import_program.program);
|
|
337
402
|
addMergeReportsCommand(import_program.program);
|
|
338
403
|
addClearCacheCommand(import_program.program);
|
|
404
|
+
addBrowserMCPServerCommand(import_program.program);
|
|
405
|
+
addTestMCPServerCommand(import_program.program);
|
|
339
406
|
addDevServerCommand(import_program.program);
|
|
340
407
|
addTestServerCommand(import_program.program);
|
|
408
|
+
addInitAgentsCommand(import_program.program);
|
|
341
409
|
// Annotate the CommonJS export names for ESM import in node:
|
|
342
410
|
0 && (module.exports = {
|
|
343
411
|
program
|
package/lib/reporters/base.js
CHANGED
|
@@ -120,7 +120,7 @@ class TerminalReporter {
|
|
|
120
120
|
this._fatalErrors = [];
|
|
121
121
|
this._failureCount = 0;
|
|
122
122
|
this.screen = options.screen ?? terminalScreen;
|
|
123
|
-
this.
|
|
123
|
+
this._options = options;
|
|
124
124
|
}
|
|
125
125
|
version() {
|
|
126
126
|
return "v2";
|
|
@@ -258,7 +258,7 @@ class TerminalReporter {
|
|
|
258
258
|
epilogue(full) {
|
|
259
259
|
const summary = this.generateSummary();
|
|
260
260
|
const summaryMessage = this.generateSummaryMessage(summary);
|
|
261
|
-
if (full && summary.failuresToPrint.length && !this.
|
|
261
|
+
if (full && summary.failuresToPrint.length && !this._options.omitFailures)
|
|
262
262
|
this._printFailures(summary.failuresToPrint);
|
|
263
263
|
this._printSlowTests();
|
|
264
264
|
this._printSummary(summaryMessage);
|
|
@@ -284,14 +284,14 @@ class TerminalReporter {
|
|
|
284
284
|
willRetry(test) {
|
|
285
285
|
return test.outcome() === "unexpected" && test.results.length <= test.retries;
|
|
286
286
|
}
|
|
287
|
-
formatTestTitle(test, step
|
|
288
|
-
return formatTestTitle(this.screen, this.config, test, step,
|
|
287
|
+
formatTestTitle(test, step) {
|
|
288
|
+
return formatTestTitle(this.screen, this.config, test, step, this._options);
|
|
289
289
|
}
|
|
290
290
|
formatTestHeader(test, options = {}) {
|
|
291
|
-
return formatTestHeader(this.screen, this.config, test, options);
|
|
291
|
+
return formatTestHeader(this.screen, this.config, test, { ...options, includeTestId: this._options.includeTestId });
|
|
292
292
|
}
|
|
293
293
|
formatFailure(test, index) {
|
|
294
|
-
return formatFailure(this.screen, this.config, test, index);
|
|
294
|
+
return formatFailure(this.screen, this.config, test, index, this._options);
|
|
295
295
|
}
|
|
296
296
|
formatError(error) {
|
|
297
297
|
return formatError(this.screen, error);
|
|
@@ -300,9 +300,9 @@ class TerminalReporter {
|
|
|
300
300
|
this.screen.stdout?.write(line ? line + "\n" : "\n");
|
|
301
301
|
}
|
|
302
302
|
}
|
|
303
|
-
function formatFailure(screen, config, test, index) {
|
|
303
|
+
function formatFailure(screen, config, test, index, options) {
|
|
304
304
|
const lines = [];
|
|
305
|
-
const header = formatTestHeader(screen, config, test, { indent: " ", index, mode: "error" });
|
|
305
|
+
const header = formatTestHeader(screen, config, test, { indent: " ", index, mode: "error", includeTestId: options?.includeTestId });
|
|
306
306
|
lines.push(screen.colors.red(header));
|
|
307
307
|
for (const result of test.results) {
|
|
308
308
|
const resultLines = [];
|
|
@@ -416,20 +416,18 @@ function stepSuffix(step) {
|
|
|
416
416
|
const stepTitles = step ? step.titlePath() : [];
|
|
417
417
|
return stepTitles.map((t) => t.split("\n")[0]).map((t) => " \u203A " + t).join("");
|
|
418
418
|
}
|
|
419
|
-
function formatTestTitle(screen, config, test, step,
|
|
419
|
+
function formatTestTitle(screen, config, test, step, options = {}) {
|
|
420
420
|
const [, projectName, , ...titles] = test.titlePath();
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
const projectTitle = projectName ? `[${projectName}] \u203A ` : "";
|
|
427
|
-
const testTitle = `${projectTitle}${location} \u203A ${titles.join(" \u203A ")}`;
|
|
421
|
+
const location = `${relativeTestPath(screen, config, test)}:${test.location.line}:${test.location.column}`;
|
|
422
|
+
const testId = options.includeTestId ? `[id=${test.id}] ` : "";
|
|
423
|
+
const projectLabel = options.includeTestId ? `project=` : "";
|
|
424
|
+
const projectTitle = projectName ? `[${projectLabel}${projectName}] \u203A ` : "";
|
|
425
|
+
const testTitle = `${testId}${projectTitle}${location} \u203A ${titles.join(" \u203A ")}`;
|
|
428
426
|
const extraTags = test.tags.filter((t) => !testTitle.includes(t));
|
|
429
427
|
return `${testTitle}${stepSuffix(step)}${extraTags.length ? " " + extraTags.join(" ") : ""}`;
|
|
430
428
|
}
|
|
431
429
|
function formatTestHeader(screen, config, test, options = {}) {
|
|
432
|
-
const title = formatTestTitle(screen, config, test);
|
|
430
|
+
const title = formatTestTitle(screen, config, test, void 0, options);
|
|
433
431
|
const header = `${options.indent || ""}${options.index ? options.index + ") " : ""}${title}`;
|
|
434
432
|
let fullHeader = header;
|
|
435
433
|
if (options.mode === "error") {
|
package/lib/reporters/html.js
CHANGED
|
@@ -113,7 +113,17 @@ class HtmlReporter {
|
|
|
113
113
|
else if (process.env.PLAYWRIGHT_HTML_NO_SNIPPETS)
|
|
114
114
|
noSnippets = true;
|
|
115
115
|
noSnippets = noSnippets || this._options.noSnippets;
|
|
116
|
-
|
|
116
|
+
let noCopyPrompt;
|
|
117
|
+
if (process.env.PLAYWRIGHT_HTML_NO_COPY_PROMPT === "false" || process.env.PLAYWRIGHT_HTML_NO_COPY_PROMPT === "0")
|
|
118
|
+
noCopyPrompt = false;
|
|
119
|
+
else if (process.env.PLAYWRIGHT_HTML_NO_COPY_PROMPT)
|
|
120
|
+
noCopyPrompt = true;
|
|
121
|
+
noCopyPrompt = noCopyPrompt || this._options.noCopyPrompt;
|
|
122
|
+
const builder = new HtmlBuilder(this.config, this._outputFolder, this._attachmentsBaseURL, {
|
|
123
|
+
title: process.env.PLAYWRIGHT_HTML_TITLE || this._options.title,
|
|
124
|
+
noSnippets,
|
|
125
|
+
noCopyPrompt
|
|
126
|
+
});
|
|
117
127
|
this._buildResult = await builder.build(this.config.metadata, projectSuites, result, this._topLevelErrors);
|
|
118
128
|
}
|
|
119
129
|
async onExit() {
|
|
@@ -197,41 +207,26 @@ function startHtmlReportServer(folder) {
|
|
|
197
207
|
return server;
|
|
198
208
|
}
|
|
199
209
|
class HtmlBuilder {
|
|
200
|
-
constructor(config, outputDir, attachmentsBaseURL,
|
|
210
|
+
constructor(config, outputDir, attachmentsBaseURL, options) {
|
|
201
211
|
this._stepsInFile = new import_utils.MultiMap();
|
|
202
212
|
this._hasTraces = false;
|
|
203
213
|
this._config = config;
|
|
204
214
|
this._reportFolder = outputDir;
|
|
205
|
-
this.
|
|
215
|
+
this._options = options;
|
|
206
216
|
import_fs.default.mkdirSync(this._reportFolder, { recursive: true });
|
|
207
217
|
this._dataZipFile = new import_zipBundle.yazl.ZipFile();
|
|
208
218
|
this._attachmentsBaseURL = attachmentsBaseURL;
|
|
209
|
-
this._title = title;
|
|
210
219
|
}
|
|
211
220
|
async build(metadata, projectSuites, result, topLevelErrors) {
|
|
212
221
|
const data = /* @__PURE__ */ new Map();
|
|
213
222
|
for (const projectSuite of projectSuites) {
|
|
223
|
+
const projectName = projectSuite.project().name;
|
|
214
224
|
for (const fileSuite of projectSuite.suites) {
|
|
215
225
|
const fileName = this._relativeLocation(fileSuite.location).file;
|
|
216
|
-
|
|
217
|
-
let fileEntry = data.get(fileId);
|
|
218
|
-
if (!fileEntry) {
|
|
219
|
-
fileEntry = {
|
|
220
|
-
testFile: { fileId, fileName, tests: [] },
|
|
221
|
-
testFileSummary: { fileId, fileName, tests: [], stats: emptyStats() }
|
|
222
|
-
};
|
|
223
|
-
data.set(fileId, fileEntry);
|
|
224
|
-
}
|
|
225
|
-
const { testFile, testFileSummary } = fileEntry;
|
|
226
|
-
const testEntries = [];
|
|
227
|
-
this._processSuite(fileSuite, projectSuite.project().name, [], testEntries);
|
|
228
|
-
for (const test of testEntries) {
|
|
229
|
-
testFile.tests.push(test.testCase);
|
|
230
|
-
testFileSummary.tests.push(test.testCaseSummary);
|
|
231
|
-
}
|
|
226
|
+
this._createEntryForSuite(data, projectName, fileSuite, fileName, true);
|
|
232
227
|
}
|
|
233
228
|
}
|
|
234
|
-
if (!this.
|
|
229
|
+
if (!this._options.noSnippets)
|
|
235
230
|
createSnippets(this._stepsInFile);
|
|
236
231
|
let ok = true;
|
|
237
232
|
for (const [fileId, { testFile, testFileSummary }] of data) {
|
|
@@ -260,13 +255,13 @@ class HtmlBuilder {
|
|
|
260
255
|
}
|
|
261
256
|
const htmlReport = {
|
|
262
257
|
metadata,
|
|
263
|
-
title: this._title,
|
|
264
258
|
startTime: result.startTime.getTime(),
|
|
265
259
|
duration: result.duration,
|
|
266
260
|
files: [...data.values()].map((e) => e.testFileSummary),
|
|
267
261
|
projectNames: projectSuites.map((r) => r.project().name),
|
|
268
262
|
stats: { ...[...data.values()].reduce((a, e) => addStats(a, e.testFileSummary.stats), emptyStats()) },
|
|
269
|
-
errors: topLevelErrors.map((error) => (0, import_base.formatError)(import_base.internalScreen, error).message)
|
|
263
|
+
errors: topLevelErrors.map((error) => (0, import_base.formatError)(import_base.internalScreen, error).message),
|
|
264
|
+
options: this._options
|
|
270
265
|
};
|
|
271
266
|
htmlReport.files.sort((f1, f2) => {
|
|
272
267
|
const w1 = f1.stats.unexpected * 1e3 + f1.stats.flaky;
|
|
@@ -331,13 +326,31 @@ class HtmlBuilder {
|
|
|
331
326
|
_addDataFile(fileName, data) {
|
|
332
327
|
this._dataZipFile.addBuffer(Buffer.from(JSON.stringify(data)), fileName);
|
|
333
328
|
}
|
|
334
|
-
|
|
329
|
+
_createEntryForSuite(data, projectName, suite, fileName, deep) {
|
|
330
|
+
const fileId = (0, import_utils.calculateSha1)(fileName).slice(0, 20);
|
|
331
|
+
let fileEntry = data.get(fileId);
|
|
332
|
+
if (!fileEntry) {
|
|
333
|
+
fileEntry = {
|
|
334
|
+
testFile: { fileId, fileName, tests: [] },
|
|
335
|
+
testFileSummary: { fileId, fileName, tests: [], stats: emptyStats() }
|
|
336
|
+
};
|
|
337
|
+
data.set(fileId, fileEntry);
|
|
338
|
+
}
|
|
339
|
+
const { testFile, testFileSummary } = fileEntry;
|
|
340
|
+
const testEntries = [];
|
|
341
|
+
this._processSuite(suite, projectName, [], deep, testEntries);
|
|
342
|
+
for (const test of testEntries) {
|
|
343
|
+
testFile.tests.push(test.testCase);
|
|
344
|
+
testFileSummary.tests.push(test.testCaseSummary);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
_processSuite(suite, projectName, path2, deep, outTests) {
|
|
335
348
|
const newPath = [...path2, suite.title];
|
|
336
349
|
suite.entries().forEach((e) => {
|
|
337
350
|
if (e.type === "test")
|
|
338
351
|
outTests.push(this._createTestEntry(e, projectName, newPath));
|
|
339
|
-
else
|
|
340
|
-
this._processSuite(e, projectName, newPath, outTests);
|
|
352
|
+
else if (deep)
|
|
353
|
+
this._processSuite(e, projectName, newPath, deep, outTests);
|
|
341
354
|
});
|
|
342
355
|
}
|
|
343
356
|
_createTestEntry(test, projectName, path2) {
|
package/lib/reporters/list.js
CHANGED
|
@@ -39,6 +39,7 @@ 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;
|
|
42
43
|
}
|
|
43
44
|
onBegin(suite) {
|
|
44
45
|
super.onBegin(suite);
|
|
@@ -61,11 +62,11 @@ class ListReporter extends import_base.TerminalReporter {
|
|
|
61
62
|
}
|
|
62
63
|
onStdOut(chunk, test, result) {
|
|
63
64
|
super.onStdOut(chunk, test, result);
|
|
64
|
-
this._dumpToStdio(test, chunk, this.screen.stdout);
|
|
65
|
+
this._dumpToStdio(test, chunk, this.screen.stdout, "out");
|
|
65
66
|
}
|
|
66
67
|
onStdErr(chunk, test, result) {
|
|
67
68
|
super.onStdErr(chunk, test, result);
|
|
68
|
-
this._dumpToStdio(test, chunk, this.screen.stderr);
|
|
69
|
+
this._dumpToStdio(test, chunk, this.screen.stderr, "err");
|
|
69
70
|
}
|
|
70
71
|
getStepIndex(testIndex, result, step) {
|
|
71
72
|
if (this._stepIndex.has(step))
|
|
@@ -137,12 +138,15 @@ class ListReporter extends import_base.TerminalReporter {
|
|
|
137
138
|
}
|
|
138
139
|
}
|
|
139
140
|
}
|
|
140
|
-
_dumpToStdio(test, chunk, stream) {
|
|
141
|
+
_dumpToStdio(test, chunk, stream, stdio) {
|
|
141
142
|
if (this.config.quiet)
|
|
142
143
|
return;
|
|
143
144
|
const text = chunk.toString("utf-8");
|
|
144
145
|
this._updateLineCountAndNewLineFlagForOutput(text);
|
|
145
|
-
|
|
146
|
+
if (this._prefixStdio)
|
|
147
|
+
stream.write(`[${stdio}] ${chunk}`);
|
|
148
|
+
else
|
|
149
|
+
stream.write(chunk);
|
|
146
150
|
}
|
|
147
151
|
onTestEnd(test, result) {
|
|
148
152
|
super.onTestEnd(test, result);
|
|
@@ -34,7 +34,8 @@ module.exports = __toCommonJS(listModeReporter_exports);
|
|
|
34
34
|
var import_path = __toESM(require("path"));
|
|
35
35
|
var import_base = require("./base");
|
|
36
36
|
class ListModeReporter {
|
|
37
|
-
constructor(options) {
|
|
37
|
+
constructor(options = {}) {
|
|
38
|
+
this._options = options;
|
|
38
39
|
this.screen = options?.screen ?? import_base.terminalScreen;
|
|
39
40
|
}
|
|
40
41
|
version() {
|
|
@@ -50,8 +51,10 @@ class ListModeReporter {
|
|
|
50
51
|
for (const test of tests) {
|
|
51
52
|
const [, projectName, , ...titles] = test.titlePath();
|
|
52
53
|
const location = `${import_path.default.relative(this.config.rootDir, test.location.file)}:${test.location.line}:${test.location.column}`;
|
|
53
|
-
const
|
|
54
|
-
this.
|
|
54
|
+
const testId = this._options.includeTestId ? `[id=${test.id}] ` : "";
|
|
55
|
+
const projectLabel = this._options.includeTestId ? `project=` : "";
|
|
56
|
+
const projectTitle = projectName ? `[${projectLabel}${projectName}] \u203A ` : "";
|
|
57
|
+
this._writeLine(` ${testId}${projectTitle}${location} \u203A ${titles.join(" \u203A ")}`);
|
|
55
58
|
files.add(test.location.file);
|
|
56
59
|
}
|
|
57
60
|
this._writeLine(`Total: ${tests.length} ${tests.length === 1 ? "test" : "tests"} in ${files.size} ${files.size === 1 ? "file" : "files"}`);
|
package/lib/reporters/merge.js
CHANGED
|
@@ -216,7 +216,9 @@ function mergeConfigureEvents(configureEvents, rootDirOverride) {
|
|
|
216
216
|
metadata: {},
|
|
217
217
|
rootDir: "",
|
|
218
218
|
version: "",
|
|
219
|
-
workers: 0
|
|
219
|
+
workers: 0,
|
|
220
|
+
globalSetup: null,
|
|
221
|
+
globalTeardown: null
|
|
220
222
|
};
|
|
221
223
|
for (const event of configureEvents)
|
|
222
224
|
config = mergeConfigs(config, event.params.config);
|
|
@@ -152,7 +152,9 @@ class TeleReporterEmitter {
|
|
|
152
152
|
metadata: config.metadata,
|
|
153
153
|
rootDir: config.rootDir,
|
|
154
154
|
version: config.version,
|
|
155
|
-
workers: config.workers
|
|
155
|
+
workers: config.workers,
|
|
156
|
+
globalSetup: config.globalSetup,
|
|
157
|
+
globalTeardown: config.globalTeardown
|
|
156
158
|
};
|
|
157
159
|
}
|
|
158
160
|
_serializeProject(suite) {
|
package/lib/runner/dispatcher.js
CHANGED
|
@@ -158,8 +158,14 @@ class Dispatcher {
|
|
|
158
158
|
_createWorker(testGroup, parallelIndex, loaderData) {
|
|
159
159
|
const projectConfig = this._config.projects.find((p) => p.id === testGroup.projectId);
|
|
160
160
|
const outputDir = projectConfig.project.outputDir;
|
|
161
|
-
const
|
|
162
|
-
|
|
161
|
+
const worker = new import_workerHost.WorkerHost(testGroup, {
|
|
162
|
+
parallelIndex,
|
|
163
|
+
config: loaderData,
|
|
164
|
+
extraEnv: this._extraEnvByProjectId.get(testGroup.projectId) || {},
|
|
165
|
+
outputDir,
|
|
166
|
+
pauseOnError: this._failureTracker.pauseOnError(),
|
|
167
|
+
pauseAtEnd: this._failureTracker.pauseAtEnd(projectConfig)
|
|
168
|
+
});
|
|
163
169
|
const handleOutput = (params) => {
|
|
164
170
|
const chunk = chunkFromParams(params);
|
|
165
171
|
if (worker.didFail()) {
|
|
@@ -312,24 +318,6 @@ class JobDispatcher {
|
|
|
312
318
|
steps.delete(params.stepId);
|
|
313
319
|
this._reporter.onStepEnd?.(test, result, step);
|
|
314
320
|
}
|
|
315
|
-
_onStepRecoverFromError(resumeAfterStepError, params) {
|
|
316
|
-
const data = this._dataByTestId.get(params.testId);
|
|
317
|
-
if (!data) {
|
|
318
|
-
resumeAfterStepError({ stepId: params.stepId, status: "failed" });
|
|
319
|
-
return;
|
|
320
|
-
}
|
|
321
|
-
const { steps } = data;
|
|
322
|
-
const step = steps.get(params.stepId);
|
|
323
|
-
if (!step) {
|
|
324
|
-
resumeAfterStepError({ stepId: params.stepId, status: "failed" });
|
|
325
|
-
return;
|
|
326
|
-
}
|
|
327
|
-
const testError = {
|
|
328
|
-
...params.error,
|
|
329
|
-
location: step.location
|
|
330
|
-
};
|
|
331
|
-
this._failureTracker.recoverFromStepError(params.stepId, testError, resumeAfterStepError);
|
|
332
|
-
}
|
|
333
321
|
_onAttach(params) {
|
|
334
322
|
const data = this._dataByTestId.get(params.testId);
|
|
335
323
|
if (!data) {
|
|
@@ -382,7 +370,7 @@ class JobDispatcher {
|
|
|
382
370
|
}
|
|
383
371
|
}
|
|
384
372
|
_onDone(params) {
|
|
385
|
-
if (!this._remainingByTestId.size && !this._failedTests.size && !params.fatalErrors.length && !params.skipTestsDueToSetupFailure.length && !params.fatalUnknownTestIds && !params.unexpectedExitError) {
|
|
373
|
+
if (!this._remainingByTestId.size && !this._failedTests.size && !params.fatalErrors.length && !params.skipTestsDueToSetupFailure.length && !params.fatalUnknownTestIds && !params.unexpectedExitError && !params.stoppedDueToUnhandledErrorInTestFail) {
|
|
386
374
|
this._finished({ didFail: false });
|
|
387
375
|
return;
|
|
388
376
|
}
|
|
@@ -455,13 +443,11 @@ class JobDispatcher {
|
|
|
455
443
|
})
|
|
456
444
|
};
|
|
457
445
|
worker.runTestGroup(runPayload);
|
|
458
|
-
const resumeAfterStepError = worker.resumeAfterStepError.bind(worker);
|
|
459
446
|
this._listeners = [
|
|
460
447
|
import_utils.eventsHelper.addEventListener(worker, "testBegin", this._onTestBegin.bind(this)),
|
|
461
448
|
import_utils.eventsHelper.addEventListener(worker, "testEnd", this._onTestEnd.bind(this)),
|
|
462
449
|
import_utils.eventsHelper.addEventListener(worker, "stepBegin", this._onStepBegin.bind(this)),
|
|
463
450
|
import_utils.eventsHelper.addEventListener(worker, "stepEnd", this._onStepEnd.bind(this)),
|
|
464
|
-
import_utils.eventsHelper.addEventListener(worker, "stepRecoverFromError", this._onStepRecoverFromError.bind(this, resumeAfterStepError)),
|
|
465
451
|
import_utils.eventsHelper.addEventListener(worker, "attach", this._onAttach.bind(this)),
|
|
466
452
|
import_utils.eventsHelper.addEventListener(worker, "done", this._onDone.bind(this)),
|
|
467
453
|
import_utils.eventsHelper.addEventListener(worker, "exit", this.onExit.bind(this))
|
|
@@ -22,35 +22,31 @@ __export(failureTracker_exports, {
|
|
|
22
22
|
});
|
|
23
23
|
module.exports = __toCommonJS(failureTracker_exports);
|
|
24
24
|
class FailureTracker {
|
|
25
|
-
constructor(_config) {
|
|
25
|
+
constructor(_config, options) {
|
|
26
26
|
this._config = _config;
|
|
27
27
|
this._failureCount = 0;
|
|
28
28
|
this._hasWorkerErrors = false;
|
|
29
|
+
this._topLevelProjects = [];
|
|
30
|
+
this._pauseOnError = options?.pauseOnError ?? false;
|
|
31
|
+
this._pauseAtEnd = options?.pauseAtEnd ?? false;
|
|
29
32
|
}
|
|
30
|
-
|
|
31
|
-
return !!this._recoverFromStepErrorHandler;
|
|
32
|
-
}
|
|
33
|
-
setRecoverFromStepErrorHandler(recoverFromStepErrorHandler) {
|
|
34
|
-
this._recoverFromStepErrorHandler = recoverFromStepErrorHandler;
|
|
35
|
-
}
|
|
36
|
-
onRootSuite(rootSuite) {
|
|
33
|
+
onRootSuite(rootSuite, topLevelProjects) {
|
|
37
34
|
this._rootSuite = rootSuite;
|
|
35
|
+
this._topLevelProjects = topLevelProjects;
|
|
38
36
|
}
|
|
39
37
|
onTestEnd(test, result) {
|
|
40
38
|
if (test.outcome() === "unexpected" && test.results.length > test.retries)
|
|
41
39
|
++this._failureCount;
|
|
42
40
|
}
|
|
43
|
-
recoverFromStepError(stepId, error, resumeAfterStepError) {
|
|
44
|
-
if (!this._recoverFromStepErrorHandler) {
|
|
45
|
-
resumeAfterStepError({ stepId, status: "failed" });
|
|
46
|
-
return;
|
|
47
|
-
}
|
|
48
|
-
void this._recoverFromStepErrorHandler(stepId, error).then(resumeAfterStepError).catch(() => {
|
|
49
|
-
});
|
|
50
|
-
}
|
|
51
41
|
onWorkerError() {
|
|
52
42
|
this._hasWorkerErrors = true;
|
|
53
43
|
}
|
|
44
|
+
pauseOnError() {
|
|
45
|
+
return this._pauseOnError;
|
|
46
|
+
}
|
|
47
|
+
pauseAtEnd(inProject) {
|
|
48
|
+
return this._pauseAtEnd && this._topLevelProjects.includes(inProject);
|
|
49
|
+
}
|
|
54
50
|
hasReachedMaxFailures() {
|
|
55
51
|
return this.maxFailures() > 0 && this._failureCount >= this.maxFailures();
|
|
56
52
|
}
|
package/lib/runner/loadUtils.js
CHANGED
|
@@ -32,10 +32,13 @@ __export(loadUtils_exports, {
|
|
|
32
32
|
createRootSuite: () => createRootSuite,
|
|
33
33
|
loadFileSuites: () => loadFileSuites,
|
|
34
34
|
loadGlobalHook: () => loadGlobalHook,
|
|
35
|
-
loadReporter: () => loadReporter
|
|
35
|
+
loadReporter: () => loadReporter,
|
|
36
|
+
loadTestList: () => loadTestList
|
|
36
37
|
});
|
|
37
38
|
module.exports = __toCommonJS(loadUtils_exports);
|
|
38
39
|
var import_path = __toESM(require("path"));
|
|
40
|
+
var import_fs = __toESM(require("fs"));
|
|
41
|
+
var import_utils = require("playwright-core/lib/utils");
|
|
39
42
|
var import_loaderHost = require("./loaderHost");
|
|
40
43
|
var import_util = require("../util");
|
|
41
44
|
var import_projectUtils = require("./projectUtils");
|
|
@@ -168,14 +171,17 @@ async function createRootSuite(testRun, errors, shouldFilterOnly) {
|
|
|
168
171
|
}
|
|
169
172
|
if (config.postShardTestFilters.length)
|
|
170
173
|
(0, import_suiteUtils.filterTestsRemoveEmptySuites)(rootSuite, (test) => config.postShardTestFilters.every((filter) => filter(test)));
|
|
174
|
+
const topLevelProjects = [];
|
|
171
175
|
{
|
|
172
176
|
const projectClosure2 = new Map((0, import_projectUtils.buildProjectsClosure)(rootSuite.suites.map((suite) => suite._fullProject)));
|
|
173
177
|
for (const [project, level] of projectClosure2.entries()) {
|
|
174
178
|
if (level === "dependency")
|
|
175
179
|
rootSuite._prependSuite(buildProjectSuite(project, projectSuites.get(project)));
|
|
180
|
+
else
|
|
181
|
+
topLevelProjects.push(project);
|
|
176
182
|
}
|
|
177
183
|
}
|
|
178
|
-
return rootSuite;
|
|
184
|
+
return { rootSuite, topLevelProjects };
|
|
179
185
|
}
|
|
180
186
|
function createProjectSuite(project, fileSuites) {
|
|
181
187
|
const projectSuite = new import_test.Suite(project.project.name, "project");
|
|
@@ -287,11 +293,41 @@ function sourceMapSources(file, cache) {
|
|
|
287
293
|
return sources;
|
|
288
294
|
}
|
|
289
295
|
}
|
|
296
|
+
async function loadTestList(config, filePath) {
|
|
297
|
+
try {
|
|
298
|
+
const content = await import_fs.default.promises.readFile(filePath, "utf-8");
|
|
299
|
+
const lines = content.split("\n").map((line) => line.trim()).filter((line) => line && !line.startsWith("#"));
|
|
300
|
+
const descriptions = lines.map((line) => {
|
|
301
|
+
const delimiter = line.includes("\u203A") ? "\u203A" : ">";
|
|
302
|
+
const tokens = line.split(delimiter).map((token) => token.trim());
|
|
303
|
+
let project;
|
|
304
|
+
if (tokens[0].startsWith("[")) {
|
|
305
|
+
if (!tokens[0].endsWith("]"))
|
|
306
|
+
throw new Error(`Malformed test description: ${line}`);
|
|
307
|
+
project = tokens[0].substring(1, tokens[0].length - 1);
|
|
308
|
+
tokens.shift();
|
|
309
|
+
}
|
|
310
|
+
return { project, file: (0, import_utils.toPosixPath)((0, import_util.parseLocationArg)(tokens[0]).file), titlePath: tokens.slice(1) };
|
|
311
|
+
});
|
|
312
|
+
return (test) => descriptions.some((d) => {
|
|
313
|
+
const [projectName, , ...titles] = test.titlePath();
|
|
314
|
+
if (d.project !== void 0 && d.project !== projectName)
|
|
315
|
+
return false;
|
|
316
|
+
const relativeFile = (0, import_utils.toPosixPath)(import_path.default.relative(config.config.rootDir, test.location.file));
|
|
317
|
+
if (relativeFile !== d.file)
|
|
318
|
+
return false;
|
|
319
|
+
return d.titlePath.length === titles.length && d.titlePath.every((_, index) => titles[index] === d.titlePath[index]);
|
|
320
|
+
});
|
|
321
|
+
} catch (e) {
|
|
322
|
+
throw (0, import_util.errorWithFile)(filePath, "Cannot read test list file: " + e.message);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
290
325
|
// Annotate the CommonJS export names for ESM import in node:
|
|
291
326
|
0 && (module.exports = {
|
|
292
327
|
collectProjectsAndTestFiles,
|
|
293
328
|
createRootSuite,
|
|
294
329
|
loadFileSuites,
|
|
295
330
|
loadGlobalHook,
|
|
296
|
-
loadReporter
|
|
331
|
+
loadReporter,
|
|
332
|
+
loadTestList
|
|
297
333
|
});
|
|
@@ -32,7 +32,8 @@ __export(projectUtils_exports, {
|
|
|
32
32
|
buildProjectsClosure: () => buildProjectsClosure,
|
|
33
33
|
buildTeardownToSetupsMap: () => buildTeardownToSetupsMap,
|
|
34
34
|
collectFilesForProject: () => collectFilesForProject,
|
|
35
|
-
filterProjects: () => filterProjects
|
|
35
|
+
filterProjects: () => filterProjects,
|
|
36
|
+
findTopLevelProjects: () => findTopLevelProjects
|
|
36
37
|
});
|
|
37
38
|
module.exports = __toCommonJS(projectUtils_exports);
|
|
38
39
|
var import_fs = __toESM(require("fs"));
|
|
@@ -116,6 +117,10 @@ function buildProjectsClosure(projects, hasTests) {
|
|
|
116
117
|
visit(0, p);
|
|
117
118
|
return result;
|
|
118
119
|
}
|
|
120
|
+
function findTopLevelProjects(config) {
|
|
121
|
+
const closure = buildProjectsClosure(config.projects);
|
|
122
|
+
return [...closure].filter((entry) => entry[1] === "top-level").map((entry) => entry[0]);
|
|
123
|
+
}
|
|
119
124
|
function buildDependentProjects(forProjects, projects) {
|
|
120
125
|
const reverseDeps = new Map(projects.map((p) => [p, []]));
|
|
121
126
|
for (const project of projects) {
|
|
@@ -231,5 +236,6 @@ async function collectFiles(testDir, respectGitIgnore) {
|
|
|
231
236
|
buildProjectsClosure,
|
|
232
237
|
buildTeardownToSetupsMap,
|
|
233
238
|
collectFilesForProject,
|
|
234
|
-
filterProjects
|
|
239
|
+
filterProjects,
|
|
240
|
+
findTopLevelProjects
|
|
235
241
|
});
|