@empiricalrun/playwright-utils 0.28.7 → 0.29.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +45 -0
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/index.js +1 -6
- package/dist/playwright-extensions.d.ts.map +1 -1
- package/dist/reporter/empirical-reporter.d.ts +1 -1
- package/dist/reporter/empirical-reporter.d.ts.map +1 -1
- package/dist/reporter/empirical-reporter.js +38 -38
- package/dist/reporter/uploader.d.ts +1 -12
- package/dist/reporter/uploader.d.ts.map +1 -1
- package/dist/reporter/uploader.js +0 -42
- package/dist/reporter/util.d.ts +3 -21
- package/dist/reporter/util.d.ts.map +1 -1
- package/dist/reporter/util.js +0 -17
- package/package.json +6 -6
- package/playwright.config.ts +7 -4
- package/tsconfig.tsbuildinfo +1 -1
- package/dist/reporter/base.d.ts +0 -41
- package/dist/reporter/base.d.ts.map +0 -1
- package/dist/reporter/base.js +0 -177
- package/dist/reporter/custom.d.ts +0 -72
- package/dist/reporter/custom.d.ts.map +0 -1
- package/dist/reporter/custom.js +0 -824
- package/dist/reporter/queue.d.ts +0 -8
- package/dist/reporter/queue.d.ts.map +0 -1
- package/dist/reporter/queue.js +0 -88
- package/dist/reporter/reporterV2.d.ts +0 -36
- package/dist/reporter/reporterV2.d.ts.map +0 -1
- package/dist/reporter/reporterV2.js +0 -19
- package/dist/reporter/third_party/html-reporter-types.d.ts +0 -107
- package/dist/reporter/third_party/html-reporter-types.d.ts.map +0 -1
- package/dist/reporter/third_party/html-reporter-types.js +0 -18
- package/dist/reporter/types.d.ts +0 -3
- package/dist/reporter/types.d.ts.map +0 -1
- package/dist/reporter/types.js +0 -2
- package/eslint.config.mjs +0 -23
package/dist/reporter/custom.js
DELETED
|
@@ -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;
|