playwright 1.54.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 +2727 -434
- 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 +3 -1
- package/lib/common/configLoader.js +2 -1
- package/lib/common/expectBundle.js +3 -0
- package/lib/common/expectBundleImpl.js +51 -51
- package/lib/common/fixtures.js +1 -1
- package/lib/common/suiteUtils.js +0 -9
- package/lib/index.js +127 -115
- package/lib/isomorphic/testTree.js +35 -8
- package/lib/matchers/expect.js +6 -7
- package/lib/matchers/matcherHint.js +43 -15
- package/lib/matchers/matchers.js +10 -4
- 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 -31
- 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/log.js +35 -0
- package/lib/mcp/program.js +96 -0
- package/lib/mcp/sdk/bundle.js +81 -0
- package/lib/mcp/sdk/exports.js +32 -0
- package/lib/mcp/sdk/http.js +180 -0
- package/lib/mcp/sdk/inProcessTransport.js +71 -0
- package/lib/mcp/sdk/mdb.js +208 -0
- package/lib/mcp/sdk/proxyBackend.js +128 -0
- 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 +41 -0
- package/lib/plugins/webServerPlugin.js +2 -0
- package/lib/program.js +77 -57
- package/lib/reporters/base.js +34 -29
- package/lib/reporters/dot.js +11 -11
- package/lib/reporters/github.js +2 -1
- package/lib/reporters/html.js +58 -41
- package/lib/reporters/internalReporter.js +2 -1
- package/lib/reporters/line.js +15 -15
- package/lib/reporters/list.js +24 -19
- package/lib/reporters/listModeReporter.js +69 -0
- package/lib/reporters/markdown.js +3 -3
- package/lib/reporters/merge.js +3 -1
- package/lib/reporters/teleEmitter.js +3 -1
- package/lib/runner/dispatcher.js +9 -2
- package/lib/runner/failureTracker.js +12 -2
- package/lib/runner/lastRun.js +7 -4
- package/lib/runner/loadUtils.js +46 -12
- package/lib/runner/projectUtils.js +8 -2
- package/lib/runner/reporters.js +7 -32
- package/lib/runner/tasks.js +20 -10
- package/lib/runner/testRunner.js +390 -0
- package/lib/runner/testServer.js +57 -276
- package/lib/runner/watchMode.js +5 -1
- package/lib/runner/workerHost.js +8 -6
- package/lib/transform/babelBundleImpl.js +179 -195
- package/lib/transform/compilationCache.js +22 -5
- package/lib/transform/transform.js +1 -1
- package/lib/util.js +12 -35
- package/lib/utilsBundleImpl.js +1 -1
- package/lib/worker/fixtureRunner.js +7 -2
- package/lib/worker/testInfo.js +76 -45
- package/lib/worker/testTracing.js +8 -7
- package/lib/worker/workerMain.js +12 -3
- package/package.json +10 -2
- package/types/test.d.ts +63 -44
- package/types/testReporter.d.ts +1 -1
- package/lib/runner/runner.js +0 -110
|
@@ -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
|
@@ -28,8 +28,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
28
28
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
29
|
var program_exports = {};
|
|
30
30
|
__export(program_exports, {
|
|
31
|
-
program: () => import_program2.program
|
|
32
|
-
withRunnerAndMutedWrite: () => withRunnerAndMutedWrite
|
|
31
|
+
program: () => import_program2.program
|
|
33
32
|
});
|
|
34
33
|
module.exports = __toCommonJS(program_exports);
|
|
35
34
|
var import_fs = __toESM(require("fs"));
|
|
@@ -43,10 +42,17 @@ var import_base = require("./reporters/base");
|
|
|
43
42
|
var import_html = require("./reporters/html");
|
|
44
43
|
var import_merge = require("./reporters/merge");
|
|
45
44
|
var import_projectUtils = require("./runner/projectUtils");
|
|
46
|
-
var import_runner = require("./runner/runner");
|
|
47
45
|
var testServer = __toESM(require("./runner/testServer"));
|
|
48
46
|
var import_watchMode = require("./runner/watchMode");
|
|
49
|
-
var
|
|
47
|
+
var import_testRunner = require("./runner/testRunner");
|
|
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");
|
|
50
56
|
function addTestCommand(program3) {
|
|
51
57
|
const command = program3.command("test [test-filter...]");
|
|
52
58
|
command.description("run tests with Playwright Test");
|
|
@@ -78,44 +84,24 @@ Examples:
|
|
|
78
84
|
$ npx playwright test --headed
|
|
79
85
|
$ npx playwright test --project=webkit`);
|
|
80
86
|
}
|
|
81
|
-
function addListFilesCommand(program3) {
|
|
82
|
-
const command = program3.command("list-files [file-filter...]", { hidden: true });
|
|
83
|
-
command.description("List files with Playwright Test tests");
|
|
84
|
-
command.option("-c, --config <file>", `Configuration file, or a test directory with optional "playwright.config.{m,c}?{js,ts}"`);
|
|
85
|
-
command.option("--project <project-name...>", `Only run tests from the specified list of projects, supports '*' wildcard (default: list all projects)`);
|
|
86
|
-
command.action(async (args, opts) => listTestFiles(opts));
|
|
87
|
-
}
|
|
88
87
|
function addClearCacheCommand(program3) {
|
|
89
88
|
const command = program3.command("clear-cache");
|
|
90
89
|
command.description("clears build and test caches");
|
|
91
90
|
command.option("-c, --config <file>", `Configuration file, or a test directory with optional "playwright.config.{m,c}?{js,ts}"`);
|
|
92
91
|
command.action(async (opts) => {
|
|
93
|
-
const
|
|
94
|
-
const
|
|
95
|
-
const { status } = await runner.clearCache();
|
|
92
|
+
const runner = new import_testRunner.TestRunner((0, import_configLoader.resolveConfigLocation)(opts.config), {});
|
|
93
|
+
const { status } = await runner.clearCache((0, import_reporters.createErrorCollectingReporter)(import_base.terminalScreen));
|
|
96
94
|
const exitCode = status === "interrupted" ? 130 : status === "passed" ? 0 : 1;
|
|
97
95
|
(0, import_utils.gracefullyProcessExitDoNotHang)(exitCode);
|
|
98
96
|
});
|
|
99
97
|
}
|
|
100
|
-
function addFindRelatedTestFilesCommand(program3) {
|
|
101
|
-
const command = program3.command("find-related-test-files [source-files...]", { hidden: true });
|
|
102
|
-
command.description("Returns the list of related tests to the given files");
|
|
103
|
-
command.option("-c, --config <file>", `Configuration file, or a test directory with optional "playwright.config.{m,c}?{js,ts}"`);
|
|
104
|
-
command.action(async (files, options) => {
|
|
105
|
-
const resolvedFiles = files.map((file) => import_path.default.resolve(process.cwd(), file));
|
|
106
|
-
await withRunnerAndMutedWrite(options.config, (runner) => runner.findRelatedTestFiles(resolvedFiles));
|
|
107
|
-
});
|
|
108
|
-
}
|
|
109
98
|
function addDevServerCommand(program3) {
|
|
110
99
|
const command = program3.command("dev-server", { hidden: true });
|
|
111
100
|
command.description("start dev server");
|
|
112
101
|
command.option("-c, --config <file>", `Configuration file, or a test directory with optional "playwright.config.{m,c}?{js,ts}"`);
|
|
113
102
|
command.action(async (options) => {
|
|
114
|
-
const
|
|
115
|
-
|
|
116
|
-
const { status } = await runner.runDevServer();
|
|
117
|
-
const exitCode = status === "interrupted" ? 130 : status === "passed" ? 0 : 1;
|
|
118
|
-
(0, import_utils.gracefullyProcessExitDoNotHang)(exitCode);
|
|
103
|
+
const runner = new import_testRunner.TestRunner((0, import_configLoader.resolveConfigLocation)(options.config), {});
|
|
104
|
+
await runner.startDevServer((0, import_reporters.createErrorCollectingReporter)(import_base.terminalScreen), "in-process");
|
|
119
105
|
});
|
|
120
106
|
}
|
|
121
107
|
function addTestServerCommand(program3) {
|
|
@@ -160,6 +146,60 @@ Arguments [dir]:
|
|
|
160
146
|
Examples:
|
|
161
147
|
$ npx playwright merge-reports playwright-report`);
|
|
162
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
|
+
}
|
|
163
203
|
async function runTests(args, opts) {
|
|
164
204
|
await (0, import_utils.startProfiling)();
|
|
165
205
|
const cliOverrides = overridesFromOptions(opts);
|
|
@@ -172,6 +212,8 @@ async function runTests(args, opts) {
|
|
|
172
212
|
config.cliProjectFilter = opts.project || void 0;
|
|
173
213
|
config.cliPassWithNoTests = !!opts.passWithNoTests;
|
|
174
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;
|
|
175
217
|
(0, import_projectUtils.filterProjects)(config.projects, config.cliProjectFilter);
|
|
176
218
|
if (opts.ui || opts.uiHost || opts.uiPort) {
|
|
177
219
|
if (opts.onlyChanged)
|
|
@@ -206,8 +248,7 @@ async function runTests(args, opts) {
|
|
|
206
248
|
(0, import_utils.gracefullyProcessExitDoNotHang)(exitCode2);
|
|
207
249
|
return;
|
|
208
250
|
}
|
|
209
|
-
const
|
|
210
|
-
const status = await runner.runAllTests();
|
|
251
|
+
const status = await (0, import_testRunner.runAllTestsWithConfig)(config);
|
|
211
252
|
await (0, import_utils.stopProfiling)("runner");
|
|
212
253
|
const exitCode = status === "interrupted" ? 130 : status === "passed" ? 0 : 1;
|
|
213
254
|
(0, import_utils.gracefullyProcessExitDoNotHang)(exitCode);
|
|
@@ -219,29 +260,6 @@ async function runTestServer(opts) {
|
|
|
219
260
|
const exitCode = status === "interrupted" ? 130 : status === "passed" ? 0 : 1;
|
|
220
261
|
(0, import_utils.gracefullyProcessExitDoNotHang)(exitCode);
|
|
221
262
|
}
|
|
222
|
-
async function withRunnerAndMutedWrite(configFile, callback) {
|
|
223
|
-
const stdoutWrite = process.stdout.write.bind(process.stdout);
|
|
224
|
-
process.stdout.write = (a, b, c) => process.stderr.write(a, b, c);
|
|
225
|
-
try {
|
|
226
|
-
const config = await (0, import_configLoader.loadConfigFromFile)(configFile);
|
|
227
|
-
const runner = new import_runner.Runner(config);
|
|
228
|
-
const result = await callback(runner);
|
|
229
|
-
stdoutWrite(JSON.stringify(result, void 0, 2), () => {
|
|
230
|
-
(0, import_utils.gracefullyProcessExitDoNotHang)(0);
|
|
231
|
-
});
|
|
232
|
-
} catch (e) {
|
|
233
|
-
const error = (0, import_util.serializeError)(e);
|
|
234
|
-
error.location = (0, import_base.prepareErrorStack)(e.stack).location;
|
|
235
|
-
stdoutWrite(JSON.stringify({ error }, void 0, 2), () => {
|
|
236
|
-
(0, import_utils.gracefullyProcessExitDoNotHang)(0);
|
|
237
|
-
});
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
async function listTestFiles(opts) {
|
|
241
|
-
await withRunnerAndMutedWrite(opts.config, async (runner) => {
|
|
242
|
-
return await runner.listTestFiles();
|
|
243
|
-
});
|
|
244
|
-
}
|
|
245
263
|
async function mergeReports(reportDir, opts) {
|
|
246
264
|
const configFile = opts.config;
|
|
247
265
|
const config = configFile ? await (0, import_configLoader.loadConfigFromFile)(configFile) : await (0, import_configLoader.loadEmptyConfigForMergeReports)();
|
|
@@ -366,6 +384,8 @@ const testOptions = [
|
|
|
366
384
|
["--reporter <reporter>", { description: `Reporter to use, comma-separated, can be ${import_config.builtInReporters.map((name) => `"${name}"`).join(", ")} (default: "${import_config.defaultReporter}")` }],
|
|
367
385
|
["--retries <retries>", { description: `Maximum retry count for flaky tests, zero for no retries (default: no retries)` }],
|
|
368
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.` }],
|
|
369
389
|
["--timeout <timeout>", { description: `Specify test timeout threshold in milliseconds, zero for unlimited (default: ${import_config.defaultTimeout})` }],
|
|
370
390
|
["--trace <mode>", { description: `Force tracing mode`, choices: kTraceModes }],
|
|
371
391
|
["--tsconfig <path>", { description: `Path to a single tsconfig applicable to all imported files (default: look up tsconfig for each imported file separately)` }],
|
|
@@ -379,14 +399,14 @@ const testOptions = [
|
|
|
379
399
|
];
|
|
380
400
|
addTestCommand(import_program.program);
|
|
381
401
|
addShowReportCommand(import_program.program);
|
|
382
|
-
addListFilesCommand(import_program.program);
|
|
383
402
|
addMergeReportsCommand(import_program.program);
|
|
384
403
|
addClearCacheCommand(import_program.program);
|
|
385
|
-
|
|
404
|
+
addBrowserMCPServerCommand(import_program.program);
|
|
405
|
+
addTestMCPServerCommand(import_program.program);
|
|
386
406
|
addDevServerCommand(import_program.program);
|
|
387
407
|
addTestServerCommand(import_program.program);
|
|
408
|
+
addInitAgentsCommand(import_program.program);
|
|
388
409
|
// Annotate the CommonJS export names for ESM import in node:
|
|
389
410
|
0 && (module.exports = {
|
|
390
|
-
program
|
|
391
|
-
withRunnerAndMutedWrite
|
|
411
|
+
program
|
|
392
412
|
});
|
package/lib/reporters/base.js
CHANGED
|
@@ -54,18 +54,20 @@ var import_utilsBundle2 = require("../utilsBundle");
|
|
|
54
54
|
const kOutputSymbol = Symbol("output");
|
|
55
55
|
const DEFAULT_TTY_WIDTH = 100;
|
|
56
56
|
const DEFAULT_TTY_HEIGHT = 40;
|
|
57
|
+
const originalProcessStdout = process.stdout;
|
|
58
|
+
const originalProcessStderr = process.stderr;
|
|
57
59
|
const terminalScreen = (() => {
|
|
58
|
-
let isTTY = !!
|
|
59
|
-
let ttyWidth =
|
|
60
|
-
let ttyHeight =
|
|
60
|
+
let isTTY = !!originalProcessStdout.isTTY;
|
|
61
|
+
let ttyWidth = originalProcessStdout.columns || 0;
|
|
62
|
+
let ttyHeight = originalProcessStdout.rows || 0;
|
|
61
63
|
if (process.env.PLAYWRIGHT_FORCE_TTY === "false" || process.env.PLAYWRIGHT_FORCE_TTY === "0") {
|
|
62
64
|
isTTY = false;
|
|
63
65
|
ttyWidth = 0;
|
|
64
66
|
ttyHeight = 0;
|
|
65
67
|
} else if (process.env.PLAYWRIGHT_FORCE_TTY === "true" || process.env.PLAYWRIGHT_FORCE_TTY === "1") {
|
|
66
68
|
isTTY = true;
|
|
67
|
-
ttyWidth =
|
|
68
|
-
ttyHeight =
|
|
69
|
+
ttyWidth = originalProcessStdout.columns || DEFAULT_TTY_WIDTH;
|
|
70
|
+
ttyHeight = originalProcessStdout.rows || DEFAULT_TTY_HEIGHT;
|
|
69
71
|
} else if (process.env.PLAYWRIGHT_FORCE_TTY) {
|
|
70
72
|
isTTY = true;
|
|
71
73
|
const sizeMatch = process.env.PLAYWRIGHT_FORCE_TTY.match(/^(\d+)x(\d+)$/);
|
|
@@ -92,7 +94,9 @@ const terminalScreen = (() => {
|
|
|
92
94
|
isTTY,
|
|
93
95
|
ttyWidth,
|
|
94
96
|
ttyHeight,
|
|
95
|
-
colors
|
|
97
|
+
colors,
|
|
98
|
+
stdout: originalProcessStdout,
|
|
99
|
+
stderr: originalProcessStderr
|
|
96
100
|
};
|
|
97
101
|
})();
|
|
98
102
|
const nonTerminalScreen = {
|
|
@@ -111,12 +115,12 @@ const internalScreen = {
|
|
|
111
115
|
};
|
|
112
116
|
class TerminalReporter {
|
|
113
117
|
constructor(options = {}) {
|
|
114
|
-
this.screen = terminalScreen;
|
|
115
118
|
this.totalTestCount = 0;
|
|
116
119
|
this.fileDurations = /* @__PURE__ */ new Map();
|
|
117
120
|
this._fatalErrors = [];
|
|
118
121
|
this._failureCount = 0;
|
|
119
|
-
this.
|
|
122
|
+
this.screen = options.screen ?? terminalScreen;
|
|
123
|
+
this._options = options;
|
|
120
124
|
}
|
|
121
125
|
version() {
|
|
122
126
|
return "v2";
|
|
@@ -254,48 +258,51 @@ class TerminalReporter {
|
|
|
254
258
|
epilogue(full) {
|
|
255
259
|
const summary = this.generateSummary();
|
|
256
260
|
const summaryMessage = this.generateSummaryMessage(summary);
|
|
257
|
-
if (full && summary.failuresToPrint.length && !this.
|
|
261
|
+
if (full && summary.failuresToPrint.length && !this._options.omitFailures)
|
|
258
262
|
this._printFailures(summary.failuresToPrint);
|
|
259
263
|
this._printSlowTests();
|
|
260
264
|
this._printSummary(summaryMessage);
|
|
261
265
|
}
|
|
262
266
|
_printFailures(failures) {
|
|
263
|
-
|
|
267
|
+
this.writeLine("");
|
|
264
268
|
failures.forEach((test, index) => {
|
|
265
|
-
|
|
269
|
+
this.writeLine(this.formatFailure(test, index + 1));
|
|
266
270
|
});
|
|
267
271
|
}
|
|
268
272
|
_printSlowTests() {
|
|
269
273
|
const slowTests = this.getSlowTests();
|
|
270
274
|
slowTests.forEach(([file, duration]) => {
|
|
271
|
-
|
|
275
|
+
this.writeLine(this.screen.colors.yellow(" Slow test file: ") + file + this.screen.colors.yellow(` (${(0, import_utilsBundle.ms)(duration)})`));
|
|
272
276
|
});
|
|
273
277
|
if (slowTests.length)
|
|
274
|
-
|
|
278
|
+
this.writeLine(this.screen.colors.yellow(" Consider running tests from slow files in parallel. See: https://playwright.dev/docs/test-parallel"));
|
|
275
279
|
}
|
|
276
280
|
_printSummary(summary) {
|
|
277
281
|
if (summary.trim())
|
|
278
|
-
|
|
282
|
+
this.writeLine(summary);
|
|
279
283
|
}
|
|
280
284
|
willRetry(test) {
|
|
281
285
|
return test.outcome() === "unexpected" && test.results.length <= test.retries;
|
|
282
286
|
}
|
|
283
|
-
formatTestTitle(test, step
|
|
284
|
-
return formatTestTitle(this.screen, this.config, test, step,
|
|
287
|
+
formatTestTitle(test, step) {
|
|
288
|
+
return formatTestTitle(this.screen, this.config, test, step, this._options);
|
|
285
289
|
}
|
|
286
290
|
formatTestHeader(test, options = {}) {
|
|
287
|
-
return formatTestHeader(this.screen, this.config, test, options);
|
|
291
|
+
return formatTestHeader(this.screen, this.config, test, { ...options, includeTestId: this._options.includeTestId });
|
|
288
292
|
}
|
|
289
293
|
formatFailure(test, index) {
|
|
290
|
-
return formatFailure(this.screen, this.config, test, index);
|
|
294
|
+
return formatFailure(this.screen, this.config, test, index, this._options);
|
|
291
295
|
}
|
|
292
296
|
formatError(error) {
|
|
293
297
|
return formatError(this.screen, error);
|
|
294
298
|
}
|
|
299
|
+
writeLine(line) {
|
|
300
|
+
this.screen.stdout?.write(line ? line + "\n" : "\n");
|
|
301
|
+
}
|
|
295
302
|
}
|
|
296
|
-
function formatFailure(screen, config, test, index) {
|
|
303
|
+
function formatFailure(screen, config, test, index, options) {
|
|
297
304
|
const lines = [];
|
|
298
|
-
const header = formatTestHeader(screen, config, test, { indent: " ", index, mode: "error" });
|
|
305
|
+
const header = formatTestHeader(screen, config, test, { indent: " ", index, mode: "error", includeTestId: options?.includeTestId });
|
|
299
306
|
lines.push(screen.colors.red(header));
|
|
300
307
|
for (const result of test.results) {
|
|
301
308
|
const resultLines = [];
|
|
@@ -409,20 +416,18 @@ function stepSuffix(step) {
|
|
|
409
416
|
const stepTitles = step ? step.titlePath() : [];
|
|
410
417
|
return stepTitles.map((t) => t.split("\n")[0]).map((t) => " \u203A " + t).join("");
|
|
411
418
|
}
|
|
412
|
-
function formatTestTitle(screen, config, test, step,
|
|
419
|
+
function formatTestTitle(screen, config, test, step, options = {}) {
|
|
413
420
|
const [, projectName, , ...titles] = test.titlePath();
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
const projectTitle = projectName ? `[${projectName}] \u203A ` : "";
|
|
420
|
-
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 ")}`;
|
|
421
426
|
const extraTags = test.tags.filter((t) => !testTitle.includes(t));
|
|
422
427
|
return `${testTitle}${stepSuffix(step)}${extraTags.length ? " " + extraTags.join(" ") : ""}`;
|
|
423
428
|
}
|
|
424
429
|
function formatTestHeader(screen, config, test, options = {}) {
|
|
425
|
-
const title = formatTestTitle(screen, config, test);
|
|
430
|
+
const title = formatTestTitle(screen, config, test, void 0, options);
|
|
426
431
|
const header = `${options.indent || ""}${options.index ? options.index + ") " : ""}${title}`;
|
|
427
432
|
let fullHeader = header;
|
|
428
433
|
if (options.mode === "error") {
|
package/lib/reporters/dot.js
CHANGED
|
@@ -29,53 +29,53 @@ class DotReporter extends import_base.TerminalReporter {
|
|
|
29
29
|
}
|
|
30
30
|
onBegin(suite) {
|
|
31
31
|
super.onBegin(suite);
|
|
32
|
-
|
|
32
|
+
this.writeLine(this.generateStartingMessage());
|
|
33
33
|
}
|
|
34
34
|
onStdOut(chunk, test, result) {
|
|
35
35
|
super.onStdOut(chunk, test, result);
|
|
36
36
|
if (!this.config.quiet)
|
|
37
|
-
|
|
37
|
+
this.screen.stdout.write(chunk);
|
|
38
38
|
}
|
|
39
39
|
onStdErr(chunk, test, result) {
|
|
40
40
|
super.onStdErr(chunk, test, result);
|
|
41
41
|
if (!this.config.quiet)
|
|
42
|
-
|
|
42
|
+
this.screen.stderr.write(chunk);
|
|
43
43
|
}
|
|
44
44
|
onTestEnd(test, result) {
|
|
45
45
|
super.onTestEnd(test, result);
|
|
46
46
|
if (this._counter === 80) {
|
|
47
|
-
|
|
47
|
+
this.screen.stdout.write("\n");
|
|
48
48
|
this._counter = 0;
|
|
49
49
|
}
|
|
50
50
|
++this._counter;
|
|
51
51
|
if (result.status === "skipped") {
|
|
52
|
-
|
|
52
|
+
this.screen.stdout.write(this.screen.colors.yellow("\xB0"));
|
|
53
53
|
return;
|
|
54
54
|
}
|
|
55
55
|
if (this.willRetry(test)) {
|
|
56
|
-
|
|
56
|
+
this.screen.stdout.write(this.screen.colors.gray("\xD7"));
|
|
57
57
|
return;
|
|
58
58
|
}
|
|
59
59
|
switch (test.outcome()) {
|
|
60
60
|
case "expected":
|
|
61
|
-
|
|
61
|
+
this.screen.stdout.write(this.screen.colors.green("\xB7"));
|
|
62
62
|
break;
|
|
63
63
|
case "unexpected":
|
|
64
|
-
|
|
64
|
+
this.screen.stdout.write(this.screen.colors.red(result.status === "timedOut" ? "T" : "F"));
|
|
65
65
|
break;
|
|
66
66
|
case "flaky":
|
|
67
|
-
|
|
67
|
+
this.screen.stdout.write(this.screen.colors.yellow("\xB1"));
|
|
68
68
|
break;
|
|
69
69
|
}
|
|
70
70
|
}
|
|
71
71
|
onError(error) {
|
|
72
72
|
super.onError(error);
|
|
73
|
-
|
|
73
|
+
this.writeLine("\n" + this.formatError(error).message);
|
|
74
74
|
this._counter = 0;
|
|
75
75
|
}
|
|
76
76
|
async onEnd(result) {
|
|
77
77
|
await super.onEnd(result);
|
|
78
|
-
|
|
78
|
+
this.screen.stdout.write("\n");
|
|
79
79
|
this.epilogue(true);
|
|
80
80
|
}
|
|
81
81
|
}
|
package/lib/reporters/github.js
CHANGED
|
@@ -41,7 +41,8 @@ class GitHubLogger {
|
|
|
41
41
|
_log(message, type = "notice", options = {}) {
|
|
42
42
|
message = message.replace(/\n/g, "%0A");
|
|
43
43
|
const configs = Object.entries(options).map(([key, option]) => `${key}=${option}`).join(",");
|
|
44
|
-
|
|
44
|
+
process.stdout.write((0, import_util.stripAnsiEscapes)(`::${type} ${configs}::${message}
|
|
45
|
+
`));
|
|
45
46
|
}
|
|
46
47
|
debug(message, options) {
|
|
47
48
|
this._log(message, "debug", options);
|
package/lib/reporters/html.js
CHANGED
|
@@ -76,12 +76,12 @@ class HtmlReporter {
|
|
|
76
76
|
if (reportedWarnings.has(key))
|
|
77
77
|
continue;
|
|
78
78
|
reportedWarnings.add(key);
|
|
79
|
-
|
|
80
|
-
|
|
79
|
+
writeLine(import_utils2.colors.red(`Configuration Error: HTML reporter output folder clashes with the tests output folder:`));
|
|
80
|
+
writeLine(`
|
|
81
81
|
html reporter folder: ${import_utils2.colors.bold(outputFolder)}
|
|
82
82
|
test results folder: ${import_utils2.colors.bold(project.outputDir)}`);
|
|
83
|
-
|
|
84
|
-
|
|
83
|
+
writeLine("");
|
|
84
|
+
writeLine(`HTML reporter will clear its output directory prior to being generated, which will lead to the artifact loss.
|
|
85
85
|
`);
|
|
86
86
|
}
|
|
87
87
|
}
|
|
@@ -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() {
|
|
@@ -128,9 +138,9 @@ class HtmlReporter {
|
|
|
128
138
|
const relativeReportPath = this._outputFolder === standaloneDefaultFolder() ? "" : " " + import_path.default.relative(process.cwd(), this._outputFolder);
|
|
129
139
|
const hostArg = this._host ? ` --host ${this._host}` : "";
|
|
130
140
|
const portArg = this._port ? ` --port ${this._port}` : "";
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
141
|
+
writeLine("");
|
|
142
|
+
writeLine("To open last HTML report run:");
|
|
143
|
+
writeLine(import_utils2.colors.cyan(`
|
|
134
144
|
${packageManagerCommand} playwright show-report${relativeReportPath}${hostArg}${portArg}
|
|
135
145
|
`));
|
|
136
146
|
}
|
|
@@ -145,7 +155,7 @@ function getHtmlReportOptionProcessEnv() {
|
|
|
145
155
|
if (!htmlOpenEnv)
|
|
146
156
|
return void 0;
|
|
147
157
|
if (!isHtmlReportOption(htmlOpenEnv)) {
|
|
148
|
-
|
|
158
|
+
writeLine(import_utils2.colors.red(`Configuration Error: HTML reporter Invalid value for PLAYWRIGHT_HTML_OPEN: ${htmlOpenEnv}. Valid values are: ${htmlReportOptions.join(", ")}`));
|
|
149
159
|
return void 0;
|
|
150
160
|
}
|
|
151
161
|
return htmlOpenEnv;
|
|
@@ -158,15 +168,15 @@ async function showHTMLReport(reportFolder, host = "localhost", port, testId) {
|
|
|
158
168
|
try {
|
|
159
169
|
(0, import_utils.assert)(import_fs.default.statSync(folder).isDirectory());
|
|
160
170
|
} catch (e) {
|
|
161
|
-
|
|
171
|
+
writeLine(import_utils2.colors.red(`No report found at "${folder}"`));
|
|
162
172
|
(0, import_utils.gracefullyProcessExitDoNotHang)(1);
|
|
163
173
|
return;
|
|
164
174
|
}
|
|
165
175
|
const server = startHtmlReportServer(folder);
|
|
166
176
|
await server.start({ port, host, preferredPort: port ? void 0 : 9323 });
|
|
167
177
|
let url = server.urlPrefix("human-readable");
|
|
168
|
-
|
|
169
|
-
|
|
178
|
+
writeLine("");
|
|
179
|
+
writeLine(import_utils2.colors.cyan(` Serving HTML report at ${url}. Press Ctrl+C to quit.`));
|
|
170
180
|
if (testId)
|
|
171
181
|
url += `#?testId=${testId}`;
|
|
172
182
|
url = url.replace("0.0.0.0", "localhost");
|
|
@@ -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;
|
|
@@ -287,7 +282,8 @@ class HtmlBuilder {
|
|
|
287
282
|
const popup = window.open(hmrURL);
|
|
288
283
|
const listener = (evt) => {
|
|
289
284
|
if (evt.source === popup && evt.data === "ready") {
|
|
290
|
-
|
|
285
|
+
const element = document.getElementById("playwrightReportBase64");
|
|
286
|
+
popup.postMessage(element?.textContent ?? "", hmrURL.origin);
|
|
291
287
|
window.removeEventListener("message", listener);
|
|
292
288
|
window.close();
|
|
293
289
|
}
|
|
@@ -319,24 +315,42 @@ class HtmlBuilder {
|
|
|
319
315
|
return { ok, singleTestId };
|
|
320
316
|
}
|
|
321
317
|
async _writeReportData(filePath) {
|
|
322
|
-
import_fs.default.appendFileSync(filePath, '<script
|
|
318
|
+
import_fs.default.appendFileSync(filePath, '<script id="playwrightReportBase64" type="application/zip">data:application/zip;base64,');
|
|
323
319
|
await new Promise((f) => {
|
|
324
320
|
this._dataZipFile.end(void 0, () => {
|
|
325
321
|
this._dataZipFile.outputStream.pipe(new Base64Encoder()).pipe(import_fs.default.createWriteStream(filePath, { flags: "a" })).on("close", f);
|
|
326
322
|
});
|
|
327
323
|
});
|
|
328
|
-
import_fs.default.appendFileSync(filePath,
|
|
324
|
+
import_fs.default.appendFileSync(filePath, "</script>");
|
|
329
325
|
}
|
|
330
326
|
_addDataFile(fileName, data) {
|
|
331
327
|
this._dataZipFile.addBuffer(Buffer.from(JSON.stringify(data)), fileName);
|
|
332
328
|
}
|
|
333
|
-
|
|
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) {
|
|
334
348
|
const newPath = [...path2, suite.title];
|
|
335
349
|
suite.entries().forEach((e) => {
|
|
336
350
|
if (e.type === "test")
|
|
337
351
|
outTests.push(this._createTestEntry(e, projectName, newPath));
|
|
338
|
-
else
|
|
339
|
-
this._processSuite(e, projectName, newPath, outTests);
|
|
352
|
+
else if (deep)
|
|
353
|
+
this._processSuite(e, projectName, newPath, deep, outTests);
|
|
340
354
|
});
|
|
341
355
|
}
|
|
342
356
|
_createTestEntry(test, projectName, path2) {
|
|
@@ -471,7 +485,7 @@ class HtmlBuilder {
|
|
|
471
485
|
_createTestStep(dedupedStep, result) {
|
|
472
486
|
const { step, duration, count } = dedupedStep;
|
|
473
487
|
const skipped = dedupedStep.step.annotations?.find((a) => a.type === "skip");
|
|
474
|
-
let title =
|
|
488
|
+
let title = step.title;
|
|
475
489
|
if (skipped)
|
|
476
490
|
title = `${title} (skipped${skipped.description ? ": " + skipped.description : ""})`;
|
|
477
491
|
const testStep = {
|
|
@@ -619,6 +633,9 @@ function createErrorCodeframe(message, location) {
|
|
|
619
633
|
}
|
|
620
634
|
);
|
|
621
635
|
}
|
|
636
|
+
function writeLine(line) {
|
|
637
|
+
process.stdout.write(line + "\n");
|
|
638
|
+
}
|
|
622
639
|
var html_default = HtmlReporter;
|
|
623
640
|
// Annotate the CommonJS export names for ESM import in node:
|
|
624
641
|
0 && (module.exports = {
|