@empiricalrun/playwright-utils 0.28.6 → 0.28.8

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.
@@ -1,824 +0,0 @@
1
- "use strict";
2
- /**
3
- * Original file: https://github.com/microsoft/playwright/blob/cf8c14f884b6f24966350a5f49b1580c3e183d21/packages/playwright/src/reporters/html.ts
4
- * Copyright (c) Microsoft Corporation.
5
- * Modifications copyright (c) Forge AI Private Limited
6
- *
7
- * Licensed under the Apache License, Version 2.0 (the "License");
8
- * you may not use this file except in compliance with the License.
9
- * You may obtain a copy of the License at
10
- *
11
- * http://www.apache.org/licenses/LICENSE-2.0
12
- *
13
- * Unless required by applicable law or agreed to in writing, software
14
- * distributed under the License is distributed on an "AS IS" BASIS,
15
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
- * See the License for the specific language governing permissions and
17
- * limitations under the License.
18
- */
19
- var __importDefault = (this && this.__importDefault) || function (mod) {
20
- return (mod && mod.__esModule) ? mod : { "default": mod };
21
- };
22
- Object.defineProperty(exports, "__esModule", { value: true });
23
- exports.showHTMLReport = showHTMLReport;
24
- exports.startHtmlReportServer = startHtmlReportServer;
25
- const code_frame_1 = require("@babel/code-frame");
26
- const crypto_1 = require("crypto");
27
- const fs_1 = __importDefault(require("fs"));
28
- const path_1 = __importDefault(require("path"));
29
- const utils_1 = require("playwright-core/lib/utils");
30
- // @ts-ignore
31
- const utilsBundle_1 = require("playwright-core/lib/utilsBundle");
32
- // @ts-ignore
33
- const zipBundle_1 = require("playwright-core/lib/zipBundle");
34
- const stream_1 = require("stream");
35
- const logger_1 = require("../logger");
36
- const base_1 = require("./base");
37
- const queue_1 = require("./queue");
38
- const uploader_1 = require("./uploader");
39
- const util_1 = require("./util");
40
- const htmlReportOptions = ["always", "never", "on-failure"];
41
- const isHtmlReportOption = (type) => {
42
- return htmlReportOptions.includes(type);
43
- };
44
- function listFilesRecursively(dir) {
45
- let files = [];
46
- try {
47
- const items = fs_1.default.readdirSync(dir); // Read the directory
48
- for (const item of items) {
49
- const fullPath = path_1.default.join(dir, item);
50
- // Get file stats
51
- const stats = fs_1.default.statSync(fullPath);
52
- if (stats.isDirectory()) {
53
- // Recursively list files in subdirectory
54
- files = files.concat(listFilesRecursively(fullPath));
55
- }
56
- else {
57
- // It's a file, add it to the list
58
- files.push(fullPath);
59
- }
60
- }
61
- }
62
- catch (error) {
63
- console.error(`Error reading directory ${dir}: ${error}`);
64
- }
65
- return files;
66
- }
67
- class HtmlReporter {
68
- config;
69
- suite;
70
- _options;
71
- _outputFolder;
72
- _attachmentsBaseURL;
73
- _open;
74
- _port;
75
- _host;
76
- // Appwright persistent device tests generate attachments after onTestEnd
77
- // We use the summary.json to re-process all attachments and this set ensures
78
- // we don't upload the same attachment multiple times
79
- haveSeenAttachments = new Set();
80
- _buildResult;
81
- _topLevelErrors = [];
82
- constructor(options) {
83
- this._options = options;
84
- }
85
- printsToStdio() {
86
- return false;
87
- }
88
- onStdOut() { }
89
- onStdErr() { }
90
- onStepBegin() { }
91
- onStepEnd() { }
92
- onTestBegin(test) {
93
- logger_1.logger.debug("Started test:", test.title);
94
- }
95
- version() {
96
- return "v2";
97
- }
98
- onTestEnd(test, result) {
99
- if (!process.env.TEST_RUN_GITHUB_ACTION_ID) {
100
- return;
101
- }
102
- try {
103
- const attachmentPromises = result.attachments.map((attachment) => {
104
- return this.processTestAttachment(attachment);
105
- });
106
- const testCaseRunEventTask = async () => {
107
- return Promise.all(attachmentPromises)
108
- .then((uploadedAttachments) => {
109
- logger_1.logger.debug(`Attachments for test ${test.title} are uploaded:`, uploadedAttachments);
110
- const { suites, projectName } = (0, util_1.suitesAndProjectForTest)(test);
111
- let params = {
112
- test,
113
- result,
114
- assetsURL: {
115
- trace: "",
116
- videos: [],
117
- },
118
- suites,
119
- projectName,
120
- runId: process.env.TEST_RUN_GITHUB_ACTION_ID || "",
121
- };
122
- uploadedAttachments.forEach((attachment) => {
123
- if (attachment) {
124
- Object.entries(attachment).forEach(([key, value]) => {
125
- if (key.includes("video")) {
126
- params.assetsURL.videos.push({
127
- name: `test-${test.id}-slug-${(0, crypto_1.randomUUID)()}`,
128
- url: value,
129
- });
130
- }
131
- if (key.includes("trace")) {
132
- params.assetsURL.trace = value;
133
- }
134
- });
135
- }
136
- });
137
- return (0, util_1.sendTestCaseUpdateToDashboard)(params);
138
- })
139
- .catch((error) => {
140
- logger_1.logger.error(`Error sending test case event for: ${test.title}:`, error);
141
- });
142
- };
143
- void (0, queue_1.sendTaskToQueue)(testCaseRunEventTask);
144
- }
145
- catch (e) {
146
- logger_1.logger.error(`Error in onTestEnd for ${test.title}:`, e);
147
- }
148
- }
149
- async processTestAttachment(attachment) {
150
- if (!attachment.path) {
151
- return;
152
- }
153
- if (this.haveSeenAttachments.has(attachment.path)) {
154
- return;
155
- }
156
- this.haveSeenAttachments.add(attachment.path);
157
- const relativePathToFile = path_1.default.relative(this._outputFolder + "/data", attachment.path);
158
- const relativeDirectoryPath = path_1.default.dirname(relativePathToFile);
159
- // on test gen run, we don't have PROJECT_NAME or TEST_RUN_GITHUB_ACTION_ID so we
160
- // skip uploading since we anyways handle uploads for that
161
- if (!process.env.PROJECT_NAME || !process.env.TEST_RUN_GITHUB_ACTION_ID) {
162
- return;
163
- }
164
- try {
165
- const uploadTask = (0, uploader_1.createUploadTask)({
166
- fileList: [attachment.path],
167
- outputFolder: this._outputFolder,
168
- relativePath: path_1.default.join("data", relativeDirectoryPath || ""),
169
- });
170
- return (0, queue_1.sendTaskToQueue)(uploadTask);
171
- }
172
- catch (e) {
173
- logger_1.logger.error("Error while uploading attachment", e);
174
- }
175
- }
176
- onConfigure(config) {
177
- this.config = config;
178
- }
179
- onBegin(suite) {
180
- logger_1.logger.debug("Using @empiricalrun/playwright-utils custom reporter");
181
- const { outputFolder, open, attachmentsBaseURL, host, port } = this._resolveOptions();
182
- this._outputFolder = outputFolder;
183
- this._open = open;
184
- this._host = host;
185
- this._port = port;
186
- this._attachmentsBaseURL = attachmentsBaseURL;
187
- this.suite = suite;
188
- }
189
- _resolveOptions() {
190
- const outputFolder = reportFolderFromEnv() ??
191
- (0, util_1.resolveReporterOutputPath)("playwright-report", this._options.configDir, this._options.outputFolder);
192
- return {
193
- outputFolder,
194
- open: getHtmlReportOptionProcessEnv() || this._options.open || "on-failure",
195
- attachmentsBaseURL:
196
- /* process.env.PLAYWRIGHT_HTML_ATTACHMENTS_BASE_URL || */
197
- this._options.attachmentsBaseURL || "data/",
198
- host: /* process.env.PLAYWRIGHT_HTML_HOST || */ this._options.host,
199
- port: /* process.env.PLAYWRIGHT_HTML_PORT
200
- ? +process.env.PLAYWRIGHT_HTML_PORT
201
- :*/ this._options.port,
202
- };
203
- }
204
- _isSubdirectory(parentDir, dir) {
205
- const relativePath = path_1.default.relative(parentDir, dir);
206
- return (!!relativePath &&
207
- !relativePath.startsWith("..") &&
208
- !path_1.default.isAbsolute(relativePath));
209
- }
210
- onError(error) {
211
- this._topLevelErrors.push(error);
212
- }
213
- async onEnd(result) {
214
- console.info(`Tests ended in ${(result.duration / (1000 * 60)).toFixed(2)} mins`);
215
- const projectSuites = this.suite.suites;
216
- // Commenting out this line causes the outputs to not get deleted
217
- // await removeFolders([this._outputFolder]);
218
- const builder = new HtmlBuilder(this.config, this._outputFolder, this._attachmentsBaseURL);
219
- this._buildResult = await builder.build(this.config.metadata, projectSuites, result, this._topLevelErrors);
220
- if (!process.env.PROJECT_NAME || !process.env.TEST_RUN_GITHUB_ACTION_ID) {
221
- return;
222
- }
223
- const startTime = new Date().getTime();
224
- logger_1.logger.info("Starting final report upload at: ", new Intl.DateTimeFormat("en-US", {
225
- hour: "2-digit",
226
- minute: "2-digit",
227
- second: "2-digit",
228
- }).format(startTime));
229
- try {
230
- logger_1.logger.debug("uploading path", this._outputFolder);
231
- logger_1.logger.debug("file list", listFilesRecursively(this._outputFolder));
232
- const summaryJsonPath = path_1.default.join(this._outputFolder, "summary.json");
233
- if (fs_1.default.existsSync(summaryJsonPath)) {
234
- logger_1.logger.debug("reading json to ensure all attachments are processed");
235
- const summaryJson = JSON.parse(fs_1.default.readFileSync(summaryJsonPath, "utf-8"));
236
- const attachments = getAttachmentsFromJSONReport(summaryJson);
237
- attachments.forEach(async (attachment) => {
238
- return this.processTestAttachment(attachment);
239
- });
240
- }
241
- const tracePath = this._outputFolder + "/trace";
242
- const traceExists = fs_1.default.existsSync(tracePath);
243
- logger_1.logger.debug("trace exists:", traceExists);
244
- const uploadTraceTask = (0, uploader_1.createUploadTask)({
245
- outputFolder: this._outputFolder,
246
- relativePath: "trace",
247
- });
248
- // upload index.html
249
- const uploadIndexTask = (0, uploader_1.createUploadTask)({
250
- outputFolder: this._outputFolder,
251
- fileList: [path_1.default.join(this._outputFolder, "index.html")],
252
- });
253
- // upload summary.json
254
- const uploadSummaryTask = (0, uploader_1.createUploadTask)({
255
- outputFolder: this._outputFolder,
256
- fileList: [path_1.default.join(this._outputFolder, "summary.json")],
257
- });
258
- if (traceExists) {
259
- void (0, queue_1.sendTaskToQueue)(uploadTraceTask);
260
- }
261
- void (0, queue_1.sendTaskToQueue)(uploadIndexTask);
262
- void (0, queue_1.sendTaskToQueue)(uploadSummaryTask);
263
- logger_1.logger.info("Waiting for all uploads to finish");
264
- await (0, queue_1.waitForTaskQueueToFinish)();
265
- logger_1.logger.info("All uploads finished");
266
- const endTime = new Date().getTime();
267
- logger_1.logger.info("Finished final report upload at: ", new Intl.DateTimeFormat("en-US", {
268
- hour: "2-digit",
269
- minute: "2-digit",
270
- second: "2-digit",
271
- }).format(endTime));
272
- const timeDiff = endTime - startTime;
273
- logger_1.logger.info("Time taken to upload after tests ended: ", timeDiff, "ms");
274
- }
275
- catch (e) {
276
- logger_1.logger.error("Error in uploading final report ", e);
277
- }
278
- }
279
- async onExit() {
280
- if (process.env.CI || !this._buildResult)
281
- return;
282
- const { ok, singleTestId } = this._buildResult;
283
- const shouldOpen = !this._options._isTestServer &&
284
- (this._open === "always" || (!ok && this._open === "on-failure"));
285
- if (shouldOpen) {
286
- await showHTMLReport(this._outputFolder, this._host, this._port, singleTestId);
287
- }
288
- else if (this._options._mode === "test") {
289
- const packageManagerCommand = (0, utils_1.getPackageManagerExecCommand)();
290
- const relativeReportPath = this._outputFolder === standaloneDefaultFolder()
291
- ? ""
292
- : " " + path_1.default.relative(process.cwd(), this._outputFolder);
293
- const hostArg = this._host ? ` --host ${this._host}` : "";
294
- const portArg = this._port ? ` --port ${this._port}` : "";
295
- console.log("");
296
- console.log("To open last HTML report run:");
297
- console.log(base_1.colors.cyan(`
298
- ${packageManagerCommand} playwright show-report${relativeReportPath}${hostArg}${portArg}
299
- `));
300
- }
301
- }
302
- }
303
- function getAttachmentsFromJSONReport(report) {
304
- const { suites } = report;
305
- return getAttachmentsFromSuites(suites);
306
- }
307
- function getAttachmentsFromSuites(suites) {
308
- if (!suites)
309
- return [];
310
- if (!suites.length)
311
- return [];
312
- let allAttachments = [];
313
- suites.forEach((currentSuite) => {
314
- const { specs } = currentSuite;
315
- const { suites } = currentSuite;
316
- const attachmentsFromSuites = getAttachmentsFromSuites(suites);
317
- const attachmentsFromSpecs = getAttachmentsFromSpecs(specs);
318
- allAttachments = allAttachments.concat(attachmentsFromSuites, attachmentsFromSpecs);
319
- });
320
- return allAttachments;
321
- }
322
- function getAttachmentsFromSpecs(specs) {
323
- if (!specs)
324
- return [];
325
- if (!specs.length)
326
- return [];
327
- let allAttachments = [];
328
- specs.forEach((spec) => {
329
- spec.tests.forEach((test) => {
330
- const { results } = test;
331
- results.forEach((result) => {
332
- const { attachments } = result;
333
- allAttachments = allAttachments.concat(attachments);
334
- });
335
- });
336
- });
337
- return allAttachments;
338
- }
339
- function reportFolderFromEnv() {
340
- // Note: PLAYWRIGHT_HTML_REPORT is for backwards compatibility.
341
- const envValue = false;
342
- // process.env.PLAYWRIGHT_HTML_OUTPUT_DIR ||
343
- // process.env.PLAYWRIGHT_HTML_REPORT;
344
- return envValue ? path_1.default.resolve(envValue) : undefined;
345
- }
346
- function getHtmlReportOptionProcessEnv() {
347
- // Note: PW_TEST_HTML_REPORT_OPEN is for backwards compatibility.
348
- const htmlOpenEnv = process.env.PLAYWRIGHT_HTML_OPEN || process.env.PW_TEST_HTML_REPORT_OPEN;
349
- if (!htmlOpenEnv)
350
- return undefined;
351
- if (!isHtmlReportOption(htmlOpenEnv)) {
352
- console.log(base_1.colors.red(`Configuration Error: HTML reporter Invalid value for PLAYWRIGHT_HTML_OPEN: ${htmlOpenEnv}. Valid values are: ${htmlReportOptions.join(", ")}`));
353
- return undefined;
354
- }
355
- return htmlOpenEnv;
356
- }
357
- function standaloneDefaultFolder() {
358
- return (reportFolderFromEnv() ??
359
- // cleanup this "test-results" value since this is should not be used anyways
360
- (0, util_1.resolveReporterOutputPath)("test-results", process.cwd(), undefined));
361
- }
362
- async function showHTMLReport(reportFolder, host = "localhost", port, testId) {
363
- const folder = reportFolder ?? standaloneDefaultFolder();
364
- try {
365
- (0, utils_1.assert)(fs_1.default.statSync(folder).isDirectory());
366
- }
367
- catch {
368
- console.log(base_1.colors.red(`No report found at "${folder}"`));
369
- (0, utils_1.gracefullyProcessExitDoNotHang)(1);
370
- return;
371
- }
372
- const server = startHtmlReportServer(folder);
373
- await server.start({ port, host, preferredPort: port ? undefined : 9323 });
374
- let url = server.urlPrefix("human-readable");
375
- console.log("");
376
- console.log(base_1.colors.cyan(` Serving HTML report at ${url}. Press Ctrl+C to quit.`));
377
- if (testId)
378
- url += `#?testId=${testId}`;
379
- url = url.replace("0.0.0.0", "localhost");
380
- await (0, utilsBundle_1.open)(url, { wait: true }).catch(() => { });
381
- await new Promise(() => { });
382
- }
383
- function startHtmlReportServer(folder) {
384
- const server = new utils_1.HttpServer();
385
- server.routePrefix("/", (request, response) => {
386
- let relativePath = new URL("http://localhost" + request.url).pathname;
387
- if (relativePath.startsWith("/trace/file")) {
388
- const url = new URL("http://localhost" + request.url);
389
- try {
390
- return server.serveFile(request, response, url.searchParams.get("path"));
391
- }
392
- catch {
393
- return false;
394
- }
395
- }
396
- if (relativePath.endsWith("/stall.js"))
397
- return true;
398
- if (relativePath === "/")
399
- relativePath = "/index.html";
400
- const absolutePath = path_1.default.join(folder, ...relativePath.split("/"));
401
- return server.serveFile(request, response, absolutePath);
402
- });
403
- return server;
404
- }
405
- class HtmlBuilder {
406
- _config;
407
- _reportFolder;
408
- _stepsInFile = new utils_1.MultiMap();
409
- _dataZipFile;
410
- _hasTraces = false;
411
- _attachmentsBaseURL;
412
- constructor(config, outputDir, attachmentsBaseURL) {
413
- this._config = config;
414
- this._reportFolder = outputDir;
415
- fs_1.default.mkdirSync(this._reportFolder, { recursive: true });
416
- this._dataZipFile = new zipBundle_1.yazl.ZipFile();
417
- this._attachmentsBaseURL = attachmentsBaseURL;
418
- }
419
- async build(metadata, projectSuites, result, topLevelErrors) {
420
- const data = new Map();
421
- for (const projectSuite of projectSuites) {
422
- const testDir = projectSuite.project().testDir;
423
- for (const fileSuite of projectSuite.suites) {
424
- const fileName = this._relativeLocation(fileSuite.location).file;
425
- // Preserve file ids computed off the testDir.
426
- const relativeFile = path_1.default.relative(testDir, fileSuite.location.file);
427
- const fileId = (0, utils_1.calculateSha1)((0, utils_1.toPosixPath)(relativeFile)).slice(0, 20);
428
- let fileEntry = data.get(fileId);
429
- if (!fileEntry) {
430
- fileEntry = {
431
- testFile: { fileId, fileName, tests: [] },
432
- testFileSummary: {
433
- fileId,
434
- fileName,
435
- tests: [],
436
- stats: emptyStats(),
437
- },
438
- };
439
- data.set(fileId, fileEntry);
440
- }
441
- const { testFile, testFileSummary } = fileEntry;
442
- const testEntries = [];
443
- this._processSuite(fileSuite, projectSuite.project().name, [], testEntries);
444
- for (const test of testEntries) {
445
- testFile.tests.push(test.testCase);
446
- testFileSummary.tests.push(test.testCaseSummary);
447
- }
448
- }
449
- }
450
- createSnippets(this._stepsInFile);
451
- let ok = true;
452
- for (const [fileId, { testFile, testFileSummary }] of data) {
453
- const stats = testFileSummary.stats;
454
- for (const test of testFileSummary.tests) {
455
- if (test.outcome === "expected")
456
- ++stats.expected;
457
- if (test.outcome === "skipped")
458
- ++stats.skipped;
459
- if (test.outcome === "unexpected")
460
- ++stats.unexpected;
461
- if (test.outcome === "flaky")
462
- ++stats.flaky;
463
- ++stats.total;
464
- }
465
- stats.ok = stats.unexpected + stats.flaky === 0;
466
- if (!stats.ok)
467
- ok = false;
468
- const testCaseSummaryComparator = (t1, t2) => {
469
- const w1 = (t1.outcome === "unexpected" ? 1000 : 0) +
470
- (t1.outcome === "flaky" ? 1 : 0);
471
- const w2 = (t2.outcome === "unexpected" ? 1000 : 0) +
472
- (t2.outcome === "flaky" ? 1 : 0);
473
- return w2 - w1;
474
- };
475
- testFileSummary.tests.sort(testCaseSummaryComparator);
476
- this._addDataFile(fileId + ".json", testFile);
477
- }
478
- const htmlReport = {
479
- metadata,
480
- startTime: result.startTime.getTime(),
481
- duration: result.duration,
482
- files: [...data.values()].map((e) => e.testFileSummary),
483
- projectNames: projectSuites.map((r) => r.project().name),
484
- stats: {
485
- ...[...data.values()].reduce((a, e) => addStats(a, e.testFileSummary.stats), emptyStats()),
486
- },
487
- errors: topLevelErrors.map((error) => (0, base_1.formatError)(error, true).message),
488
- };
489
- htmlReport.files.sort((f1, f2) => {
490
- const w1 = f1.stats.unexpected * 1000 + f1.stats.flaky;
491
- const w2 = f2.stats.unexpected * 1000 + f2.stats.flaky;
492
- return w2 - w1;
493
- });
494
- this._addDataFile("report.json", htmlReport);
495
- // Copy app.
496
- const appFolder = path_1.default.join(require.resolve("playwright-core"), "..", "lib", "vite", "htmlReport");
497
- await (0, utils_1.copyFileAndMakeWritable)(path_1.default.join(appFolder, "index.html"), path_1.default.join(this._reportFolder, "index.html"));
498
- // Copy trace viewer.
499
- if (this._hasTraces) {
500
- const traceViewerFolder = path_1.default.join(require.resolve("playwright-core"), "..", "lib", "vite", "traceViewer");
501
- const traceViewerTargetFolder = path_1.default.join(this._reportFolder, "trace");
502
- const traceViewerAssetsTargetFolder = path_1.default.join(traceViewerTargetFolder, "assets");
503
- fs_1.default.mkdirSync(traceViewerAssetsTargetFolder, { recursive: true });
504
- for (const file of fs_1.default.readdirSync(traceViewerFolder)) {
505
- if (file.endsWith(".map") ||
506
- file.includes("watch") ||
507
- file.includes("assets"))
508
- continue;
509
- await (0, utils_1.copyFileAndMakeWritable)(path_1.default.join(traceViewerFolder, file), path_1.default.join(traceViewerTargetFolder, file));
510
- }
511
- for (const file of fs_1.default.readdirSync(path_1.default.join(traceViewerFolder, "assets"))) {
512
- if (file.endsWith(".map") || file.includes("xtermModule"))
513
- continue;
514
- await (0, utils_1.copyFileAndMakeWritable)(path_1.default.join(traceViewerFolder, "assets", file), path_1.default.join(traceViewerAssetsTargetFolder, file));
515
- }
516
- }
517
- // Inline report data.
518
- const indexFile = path_1.default.join(this._reportFolder, "index.html");
519
- fs_1.default.appendFileSync(indexFile, '<script>\nwindow.playwrightReportBase64 = "data:application/zip;base64,');
520
- await new Promise((f) => {
521
- this._dataZipFile.end(undefined, () => {
522
- this._dataZipFile.outputStream.pipe(new Base64Encoder())
523
- .pipe(fs_1.default.createWriteStream(indexFile, { flags: "a" }))
524
- .on("close", f);
525
- });
526
- });
527
- fs_1.default.appendFileSync(indexFile, '";</script>');
528
- let singleTestId;
529
- if (htmlReport.stats.total === 1) {
530
- const testFile = data.values().next()
531
- ?.value?.testFile;
532
- singleTestId = testFile?.tests[0]?.testId;
533
- }
534
- return { ok, singleTestId };
535
- }
536
- _addDataFile(fileName, data) {
537
- this._dataZipFile.addBuffer(Buffer.from(JSON.stringify(data)), fileName);
538
- }
539
- _processSuite(suite, projectName, path, outTests) {
540
- const newPath = [...path, suite.title];
541
- suite.entries().forEach((e) => {
542
- if (e.type === "test")
543
- outTests.push(this._createTestEntry(e, projectName, newPath));
544
- else
545
- this._processSuite(e, projectName, newPath, outTests);
546
- });
547
- }
548
- _createTestEntry(test, projectName, path) {
549
- const duration = test.results.reduce((a, r) => a + r.duration, 0);
550
- const location = this._relativeLocation(test.location);
551
- path = path.slice(1).filter((path) => path.length > 0);
552
- const results = test.results.map((r) => this._createTestResult(test, r));
553
- return {
554
- testCase: {
555
- testId: test.id,
556
- title: test.title,
557
- projectName,
558
- location,
559
- duration,
560
- // Annotations can be pushed directly, with a wrong type.
561
- annotations: test.annotations.map((a) => ({
562
- type: a.type,
563
- description: a.description ? String(a.description) : a.description,
564
- })),
565
- tags: test.tags,
566
- outcome: test.outcome(),
567
- path,
568
- results,
569
- ok: test.outcome() === "expected" || test.outcome() === "flaky",
570
- },
571
- testCaseSummary: {
572
- testId: test.id,
573
- title: test.title,
574
- projectName,
575
- location,
576
- duration,
577
- // Annotations can be pushed directly, with a wrong type.
578
- annotations: test.annotations.map((a) => ({
579
- type: a.type,
580
- description: a.description ? String(a.description) : a.description,
581
- })),
582
- tags: test.tags,
583
- outcome: test.outcome(),
584
- path,
585
- ok: test.outcome() === "expected" || test.outcome() === "flaky",
586
- results: results.map((result) => {
587
- return {
588
- attachments: result.attachments.map((a) => ({
589
- name: a.name,
590
- contentType: a.contentType,
591
- path: a.path,
592
- })),
593
- };
594
- }),
595
- },
596
- };
597
- }
598
- _serializeAttachments(attachments) {
599
- let lastAttachment;
600
- return attachments
601
- .map((a) => {
602
- if (a.name === "trace")
603
- this._hasTraces = true;
604
- if ((a.name === "stdout" || a.name === "stderr") &&
605
- a.contentType === "text/plain") {
606
- if (lastAttachment &&
607
- lastAttachment.name === a.name &&
608
- lastAttachment.contentType === a.contentType) {
609
- lastAttachment.body += (0, base_1.stripAnsiEscapes)(a.body);
610
- return null;
611
- }
612
- a.body = (0, base_1.stripAnsiEscapes)(a.body);
613
- lastAttachment = a;
614
- return a;
615
- }
616
- if (a.path) {
617
- // in our custom reporter, we do not delete the original files onEnd
618
- const relativePath = path_1.default.relative(this._reportFolder, a.path);
619
- // removed logic which renamed original files to different versions with SHAs
620
- return {
621
- name: a.name,
622
- contentType: a.contentType,
623
- path: relativePath,
624
- body: a.body,
625
- };
626
- }
627
- if (a.body instanceof Buffer) {
628
- if (isTextContentType(a.contentType)) {
629
- // Content type is like this: "text/html; charset=UTF-8"
630
- const charset = a.contentType.match(/charset=(.*)/)?.[1];
631
- try {
632
- const body = a.body.toString(charset || "utf-8");
633
- return {
634
- name: a.name,
635
- contentType: a.contentType,
636
- body,
637
- };
638
- }
639
- catch {
640
- // Invalid encoding, fall through and save to file.
641
- }
642
- }
643
- fs_1.default.mkdirSync(path_1.default.join(this._reportFolder, "data"), {
644
- recursive: true,
645
- });
646
- const extension = (0, utils_1.sanitizeForFilePath)(path_1.default.extname(a.name).replace(/^\./, "")) ||
647
- utilsBundle_1.mime.getExtension(a.contentType) ||
648
- "dat";
649
- const sha1 = (0, utils_1.calculateSha1)(a.body) + "." + extension;
650
- fs_1.default.writeFileSync(path_1.default.join(this._reportFolder, "data", sha1), a.body);
651
- return {
652
- name: a.name,
653
- contentType: a.contentType,
654
- path: this._attachmentsBaseURL + sha1,
655
- };
656
- }
657
- // string
658
- return {
659
- name: a.name,
660
- contentType: a.contentType,
661
- body: a.body,
662
- };
663
- })
664
- .filter(Boolean);
665
- }
666
- _createTestResult(test, result) {
667
- return {
668
- duration: result.duration,
669
- startTime: result.startTime.toISOString(),
670
- retry: result.retry,
671
- steps: dedupeSteps(result.steps).map((s) => this._createTestStep(s)),
672
- errors: (0, base_1.formatResultFailure)(test, result, "", true).map((error) => error.message),
673
- status: result.status,
674
- attachments: this._serializeAttachments([
675
- ...result.attachments,
676
- ...result.stdout.map((m) => stdioAttachment(m, "stdout")),
677
- ...result.stderr.map((m) => stdioAttachment(m, "stderr")),
678
- ]),
679
- };
680
- }
681
- _createTestStep(dedupedStep) {
682
- const { step, duration, count } = dedupedStep;
683
- const result = {
684
- title: step.title,
685
- startTime: step.startTime.toISOString(),
686
- duration,
687
- steps: dedupeSteps(step.steps).map((s) => this._createTestStep(s)),
688
- location: this._relativeLocation(step.location),
689
- error: step.error?.message,
690
- count,
691
- };
692
- if (result.location)
693
- this._stepsInFile.set(result.location.file, result);
694
- return result;
695
- }
696
- _relativeLocation(location) {
697
- if (!location)
698
- return undefined;
699
- const file = (0, utils_1.toPosixPath)(path_1.default.relative(this._config.rootDir, location.file));
700
- return {
701
- file,
702
- line: location.line,
703
- column: location.column,
704
- };
705
- }
706
- }
707
- const emptyStats = () => {
708
- return {
709
- total: 0,
710
- expected: 0,
711
- unexpected: 0,
712
- flaky: 0,
713
- skipped: 0,
714
- ok: true,
715
- };
716
- };
717
- const addStats = (stats, delta) => {
718
- stats.total += delta.total;
719
- stats.skipped += delta.skipped;
720
- stats.expected += delta.expected;
721
- stats.unexpected += delta.unexpected;
722
- stats.flaky += delta.flaky;
723
- stats.ok = stats.ok && delta.ok;
724
- return stats;
725
- };
726
- class Base64Encoder extends stream_1.Transform {
727
- _remainder;
728
- _transform(chunk, encoding, callback) {
729
- if (this._remainder) {
730
- chunk = Buffer.concat([this._remainder, chunk]);
731
- this._remainder = undefined;
732
- }
733
- const remaining = chunk.length % 3;
734
- if (remaining) {
735
- this._remainder = chunk.slice(chunk.length - remaining);
736
- chunk = chunk.slice(0, chunk.length - remaining);
737
- }
738
- chunk = chunk.toString("base64");
739
- this.push(Buffer.from(chunk));
740
- callback();
741
- }
742
- _flush(callback) {
743
- if (this._remainder)
744
- this.push(Buffer.from(this._remainder.toString("base64")));
745
- callback();
746
- }
747
- }
748
- function isTextContentType(contentType) {
749
- return (contentType.startsWith("text/") ||
750
- contentType.startsWith("application/json"));
751
- }
752
- function stdioAttachment(chunk, type) {
753
- if (typeof chunk === "string") {
754
- return {
755
- name: type,
756
- contentType: "text/plain",
757
- body: chunk,
758
- };
759
- }
760
- return {
761
- name: type,
762
- contentType: "application/octet-stream",
763
- body: chunk,
764
- };
765
- }
766
- function dedupeSteps(steps) {
767
- const result = [];
768
- let lastResult = undefined;
769
- for (const step of steps) {
770
- const canDedupe = !step.error &&
771
- step.duration >= 0 &&
772
- step.location?.file &&
773
- !step.steps.length;
774
- const lastStep = lastResult?.step;
775
- if (canDedupe &&
776
- lastResult &&
777
- lastStep &&
778
- step.category === lastStep.category &&
779
- step.title === lastStep.title &&
780
- step.location?.file === lastStep.location?.file &&
781
- step.location?.line === lastStep.location?.line &&
782
- step.location?.column === lastStep.location?.column) {
783
- ++lastResult.count;
784
- lastResult.duration += step.duration;
785
- continue;
786
- }
787
- lastResult = { step, count: 1, duration: step.duration };
788
- result.push(lastResult);
789
- if (!canDedupe)
790
- lastResult = undefined;
791
- }
792
- return result;
793
- }
794
- function createSnippets(stepsInFile) {
795
- for (const file of stepsInFile.keys()) {
796
- let source;
797
- try {
798
- source = fs_1.default.readFileSync(file, "utf-8") + "\n//";
799
- }
800
- catch {
801
- continue;
802
- }
803
- const lines = source.split("\n").length;
804
- const highlighted = (0, code_frame_1.codeFrameColumns)(source, { start: { line: lines, column: 1 } }, { highlightCode: true, linesAbove: lines, linesBelow: 0 });
805
- const highlightedLines = highlighted.split("\n");
806
- const lineWithArrow = highlightedLines[highlightedLines.length - 1];
807
- for (const step of stepsInFile.get(file)) {
808
- // Don't bother with snippets that have less than 3 lines.
809
- if (step.location.line < 2 || step.location.line >= lines)
810
- continue;
811
- // Cut out snippet.
812
- const snippetLines = highlightedLines.slice(step.location.line - 2, step.location.line + 1);
813
- // Relocate arrow.
814
- const index = lineWithArrow?.indexOf("^");
815
- const shiftedArrow = lineWithArrow?.slice(0, index) +
816
- " ".repeat(step.location.column - 1) +
817
- lineWithArrow?.slice(index);
818
- // Insert arrow line.
819
- snippetLines.splice(2, 0, shiftedArrow);
820
- step.snippet = snippetLines.join("\n");
821
- }
822
- }
823
- }
824
- exports.default = HtmlReporter;