html.js 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600
  1. "use strict";
  2. Object.defineProperty(exports, "__esModule", {
  3. value: true
  4. });
  5. exports.default = void 0;
  6. exports.showHTMLReport = showHTMLReport;
  7. exports.startHtmlReportServer = startHtmlReportServer;
  8. var _utilsBundle = require("playwright-core/lib/utilsBundle");
  9. var _utils = require("playwright-core/lib/utils");
  10. var _fs = _interopRequireDefault(require("fs"));
  11. var _path = _interopRequireDefault(require("path"));
  12. var _stream = require("stream");
  13. var _json = require("./json");
  14. var _babelBundle = require("../transform/babelBundle");
  15. var _base = require("./base");
  16. var _util = require("../util");
  17. var _zipBundle = require("playwright-core/lib/zipBundle");
  18. var _config = require("../common/config");
  19. var _empty = _interopRequireDefault(require("./empty"));
  20. function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
  21. /**
  22. * Copyright (c) Microsoft Corporation.
  23. *
  24. * Licensed under the Apache License, Version 2.0 (the "License");
  25. * you may not use this file except in compliance with the License.
  26. * You may obtain a copy of the License at
  27. *
  28. * http://www.apache.org/licenses/LICENSE-2.0
  29. *
  30. * Unless required by applicable law or agreed to in writing, software
  31. * distributed under the License is distributed on an "AS IS" BASIS,
  32. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  33. * See the License for the specific language governing permissions and
  34. * limitations under the License.
  35. */
  36. const htmlReportOptions = ['always', 'never', 'on-failure'];
  37. const isHtmlReportOption = type => {
  38. return htmlReportOptions.includes(type);
  39. };
  40. class HtmlReporter extends _empty.default {
  41. constructor(options) {
  42. super();
  43. this.config = void 0;
  44. this.suite = void 0;
  45. this._options = void 0;
  46. this._outputFolder = void 0;
  47. this._attachmentsBaseURL = void 0;
  48. this._open = void 0;
  49. this._buildResult = void 0;
  50. this._topLevelErrors = [];
  51. this._options = options;
  52. }
  53. printsToStdio() {
  54. return false;
  55. }
  56. onConfigure(config) {
  57. this.config = config;
  58. }
  59. onBegin(suite) {
  60. const {
  61. outputFolder,
  62. open,
  63. attachmentsBaseURL
  64. } = this._resolveOptions();
  65. this._outputFolder = outputFolder;
  66. this._open = open;
  67. this._attachmentsBaseURL = attachmentsBaseURL;
  68. const reportedWarnings = new Set();
  69. for (const project of this.config.projects) {
  70. if (outputFolder.startsWith(project.outputDir) || project.outputDir.startsWith(outputFolder)) {
  71. const key = outputFolder + '|' + project.outputDir;
  72. if (reportedWarnings.has(key)) continue;
  73. reportedWarnings.add(key);
  74. console.log(_base.colors.red(`Configuration Error: HTML reporter output folder clashes with the tests output folder:`));
  75. console.log(`
  76. html reporter folder: ${_base.colors.bold(outputFolder)}
  77. test results folder: ${_base.colors.bold(project.outputDir)}`);
  78. console.log('');
  79. console.log(`HTML reporter will clear its output directory prior to being generated, which will lead to the artifact loss.
  80. `);
  81. }
  82. }
  83. this.suite = suite;
  84. }
  85. _resolveOptions() {
  86. var _reportFolderFromEnv;
  87. const outputFolder = (_reportFolderFromEnv = reportFolderFromEnv()) !== null && _reportFolderFromEnv !== void 0 ? _reportFolderFromEnv : (0, _util.resolveReporterOutputPath)('playwright-report', this._options.configDir, this._options.outputFolder);
  88. return {
  89. outputFolder,
  90. open: getHtmlReportOptionProcessEnv() || this._options.open || 'on-failure',
  91. attachmentsBaseURL: this._options.attachmentsBaseURL || 'data/'
  92. };
  93. }
  94. onError(error) {
  95. this._topLevelErrors.push(error);
  96. }
  97. async onEnd(result) {
  98. const projectSuites = this.suite.suites;
  99. await (0, _utils.removeFolders)([this._outputFolder]);
  100. const builder = new HtmlBuilder(this.config, this._outputFolder, this._attachmentsBaseURL);
  101. this._buildResult = await builder.build(this.config.metadata, projectSuites, result, this._topLevelErrors);
  102. }
  103. async onExit() {
  104. var _FullConfigInternal$f;
  105. if (process.env.CI || !this._buildResult) return;
  106. const {
  107. ok,
  108. singleTestId
  109. } = this._buildResult;
  110. const shouldOpen = this._open === 'always' || !ok && this._open === 'on-failure';
  111. if (shouldOpen) {
  112. await showHTMLReport(this._outputFolder, this._options.host, this._options.port, singleTestId);
  113. } else if (!((_FullConfigInternal$f = _config.FullConfigInternal.from(this.config)) !== null && _FullConfigInternal$f !== void 0 && _FullConfigInternal$f.cliListOnly)) {
  114. const packageManagerCommand = (0, _utils.getPackageManagerExecCommand)();
  115. const relativeReportPath = this._outputFolder === standaloneDefaultFolder() ? '' : ' ' + _path.default.relative(process.cwd(), this._outputFolder);
  116. const hostArg = this._options.host ? ` --host ${this._options.host}` : '';
  117. const portArg = this._options.port ? ` --port ${this._options.port}` : '';
  118. console.log('');
  119. console.log('To open last HTML report run:');
  120. console.log(_base.colors.cyan(`
  121. ${packageManagerCommand} playwright show-report${relativeReportPath}${hostArg}${portArg}
  122. `));
  123. }
  124. }
  125. }
  126. function reportFolderFromEnv() {
  127. if (process.env[`PLAYWRIGHT_HTML_REPORT`]) return _path.default.resolve(process.cwd(), process.env[`PLAYWRIGHT_HTML_REPORT`]);
  128. return undefined;
  129. }
  130. function getHtmlReportOptionProcessEnv() {
  131. const processKey = 'PW_TEST_HTML_REPORT_OPEN';
  132. const htmlOpenEnv = process.env[processKey];
  133. if (!htmlOpenEnv) return undefined;
  134. if (!isHtmlReportOption(htmlOpenEnv)) {
  135. console.log(_base.colors.red(`Configuration Error: HTML reporter Invalid value for ${processKey}: ${htmlOpenEnv}. Valid values are: ${htmlReportOptions.join(', ')}`));
  136. return undefined;
  137. }
  138. return htmlOpenEnv;
  139. }
  140. function standaloneDefaultFolder() {
  141. var _reportFolderFromEnv2;
  142. return (_reportFolderFromEnv2 = reportFolderFromEnv()) !== null && _reportFolderFromEnv2 !== void 0 ? _reportFolderFromEnv2 : (0, _util.resolveReporterOutputPath)('playwright-report', process.cwd(), undefined);
  143. }
  144. async function showHTMLReport(reportFolder, host = 'localhost', port, testId) {
  145. const folder = reportFolder !== null && reportFolder !== void 0 ? reportFolder : standaloneDefaultFolder();
  146. try {
  147. (0, _utils.assert)(_fs.default.statSync(folder).isDirectory());
  148. } catch (e) {
  149. console.log(_base.colors.red(`No report found at "${folder}"`));
  150. (0, _utils.gracefullyProcessExitDoNotHang)(1);
  151. return;
  152. }
  153. const server = startHtmlReportServer(folder);
  154. let url = await server.start({
  155. port,
  156. host,
  157. preferredPort: port ? undefined : 9323
  158. });
  159. console.log('');
  160. console.log(_base.colors.cyan(` Serving HTML report at ${url}. Press Ctrl+C to quit.`));
  161. if (testId) url += `#?testId=${testId}`;
  162. await (0, _utilsBundle.open)(url, {
  163. wait: true
  164. }).catch(() => {});
  165. await new Promise(() => {});
  166. }
  167. function startHtmlReportServer(folder) {
  168. const server = new _utils.HttpServer();
  169. server.routePrefix('/', (request, response) => {
  170. let relativePath = new URL('http://localhost' + request.url).pathname;
  171. if (relativePath.startsWith('/trace/file')) {
  172. const url = new URL('http://localhost' + request.url);
  173. try {
  174. return server.serveFile(request, response, url.searchParams.get('path'));
  175. } catch (e) {
  176. return false;
  177. }
  178. }
  179. if (relativePath.endsWith('/stall.js')) return true;
  180. if (relativePath === '/') relativePath = '/index.html';
  181. const absolutePath = _path.default.join(folder, ...relativePath.split('/'));
  182. return server.serveFile(request, response, absolutePath);
  183. });
  184. return server;
  185. }
  186. class HtmlBuilder {
  187. constructor(config, outputDir, attachmentsBaseURL) {
  188. this._config = void 0;
  189. this._reportFolder = void 0;
  190. this._stepsInFile = new _utils.MultiMap();
  191. this._dataZipFile = void 0;
  192. this._hasTraces = false;
  193. this._attachmentsBaseURL = void 0;
  194. this._config = config;
  195. this._reportFolder = outputDir;
  196. _fs.default.mkdirSync(this._reportFolder, {
  197. recursive: true
  198. });
  199. this._dataZipFile = new _zipBundle.yazl.ZipFile();
  200. this._attachmentsBaseURL = attachmentsBaseURL;
  201. }
  202. async build(metadata, projectSuites, result, topLevelErrors) {
  203. const data = new Map();
  204. for (const projectSuite of projectSuites) {
  205. for (const fileSuite of projectSuite.suites) {
  206. var _metadata;
  207. const fileName = this._relativeLocation(fileSuite.location).file;
  208. const fileId = fileSuite._fileId;
  209. let fileEntry = data.get(fileId);
  210. if (!fileEntry) {
  211. fileEntry = {
  212. testFile: {
  213. fileId,
  214. fileName,
  215. tests: []
  216. },
  217. testFileSummary: {
  218. fileId,
  219. fileName,
  220. tests: [],
  221. stats: emptyStats()
  222. }
  223. };
  224. data.set(fileId, fileEntry);
  225. }
  226. const {
  227. testFile,
  228. testFileSummary
  229. } = fileEntry;
  230. const testEntries = [];
  231. this._processJsonSuite(fileSuite, fileId, projectSuite.project().name, (_metadata = projectSuite.project().metadata) === null || _metadata === void 0 ? void 0 : _metadata.reportName, [], testEntries);
  232. for (const test of testEntries) {
  233. testFile.tests.push(test.testCase);
  234. testFileSummary.tests.push(test.testCaseSummary);
  235. }
  236. }
  237. }
  238. createSnippets(this._stepsInFile);
  239. let ok = true;
  240. for (const [fileId, {
  241. testFile,
  242. testFileSummary
  243. }] of data) {
  244. const stats = testFileSummary.stats;
  245. for (const test of testFileSummary.tests) {
  246. if (test.outcome === 'expected') ++stats.expected;
  247. if (test.outcome === 'skipped') ++stats.skipped;
  248. if (test.outcome === 'unexpected') ++stats.unexpected;
  249. if (test.outcome === 'flaky') ++stats.flaky;
  250. ++stats.total;
  251. }
  252. stats.ok = stats.unexpected + stats.flaky === 0;
  253. if (!stats.ok) ok = false;
  254. const testCaseSummaryComparator = (t1, t2) => {
  255. const w1 = (t1.outcome === 'unexpected' ? 1000 : 0) + (t1.outcome === 'flaky' ? 1 : 0);
  256. const w2 = (t2.outcome === 'unexpected' ? 1000 : 0) + (t2.outcome === 'flaky' ? 1 : 0);
  257. return w2 - w1;
  258. };
  259. testFileSummary.tests.sort(testCaseSummaryComparator);
  260. this._addDataFile(fileId + '.json', testFile);
  261. }
  262. const htmlReport = {
  263. metadata,
  264. startTime: result.startTime.getTime(),
  265. duration: result.duration,
  266. files: [...data.values()].map(e => e.testFileSummary),
  267. projectNames: projectSuites.map(r => r.project().name),
  268. stats: {
  269. ...[...data.values()].reduce((a, e) => addStats(a, e.testFileSummary.stats), emptyStats())
  270. },
  271. errors: topLevelErrors.map(error => (0, _base.formatError)(error, true).message)
  272. };
  273. htmlReport.files.sort((f1, f2) => {
  274. const w1 = f1.stats.unexpected * 1000 + f1.stats.flaky;
  275. const w2 = f2.stats.unexpected * 1000 + f2.stats.flaky;
  276. return w2 - w1;
  277. });
  278. this._addDataFile('report.json', htmlReport);
  279. // Copy app.
  280. const appFolder = _path.default.join(require.resolve('playwright-core'), '..', 'lib', 'vite', 'htmlReport');
  281. await (0, _utils.copyFileAndMakeWritable)(_path.default.join(appFolder, 'index.html'), _path.default.join(this._reportFolder, 'index.html'));
  282. // Copy trace viewer.
  283. if (this._hasTraces) {
  284. const traceViewerFolder = _path.default.join(require.resolve('playwright-core'), '..', 'lib', 'vite', 'traceViewer');
  285. const traceViewerTargetFolder = _path.default.join(this._reportFolder, 'trace');
  286. const traceViewerAssetsTargetFolder = _path.default.join(traceViewerTargetFolder, 'assets');
  287. _fs.default.mkdirSync(traceViewerAssetsTargetFolder, {
  288. recursive: true
  289. });
  290. for (const file of _fs.default.readdirSync(traceViewerFolder)) {
  291. if (file.endsWith('.map') || file.includes('watch') || file.includes('assets')) continue;
  292. await (0, _utils.copyFileAndMakeWritable)(_path.default.join(traceViewerFolder, file), _path.default.join(traceViewerTargetFolder, file));
  293. }
  294. for (const file of _fs.default.readdirSync(_path.default.join(traceViewerFolder, 'assets'))) {
  295. if (file.endsWith('.map') || file.includes('xtermModule')) continue;
  296. await (0, _utils.copyFileAndMakeWritable)(_path.default.join(traceViewerFolder, 'assets', file), _path.default.join(traceViewerAssetsTargetFolder, file));
  297. }
  298. }
  299. // Inline report data.
  300. const indexFile = _path.default.join(this._reportFolder, 'index.html');
  301. _fs.default.appendFileSync(indexFile, '<script>\nwindow.playwrightReportBase64 = "data:application/zip;base64,');
  302. await new Promise(f => {
  303. this._dataZipFile.end(undefined, () => {
  304. this._dataZipFile.outputStream.pipe(new Base64Encoder()).pipe(_fs.default.createWriteStream(indexFile, {
  305. flags: 'a'
  306. })).on('close', f);
  307. });
  308. });
  309. _fs.default.appendFileSync(indexFile, '";</script>');
  310. let singleTestId;
  311. if (htmlReport.stats.total === 1) {
  312. const testFile = data.values().next().value.testFile;
  313. singleTestId = testFile.tests[0].testId;
  314. }
  315. return {
  316. ok,
  317. singleTestId
  318. };
  319. }
  320. _addDataFile(fileName, data) {
  321. this._dataZipFile.addBuffer(Buffer.from(JSON.stringify(data)), fileName);
  322. }
  323. _processJsonSuite(suite, fileId, projectName, botName, path, outTests) {
  324. const newPath = [...path, suite.title];
  325. suite.suites.forEach(s => this._processJsonSuite(s, fileId, projectName, botName, newPath, outTests));
  326. suite.tests.forEach(t => outTests.push(this._createTestEntry(t, projectName, botName, newPath)));
  327. }
  328. _createTestEntry(test, projectName, botName, path) {
  329. const duration = test.results.reduce((a, r) => a + r.duration, 0);
  330. const location = this._relativeLocation(test.location);
  331. path = path.slice(1);
  332. const results = test.results.map(r => this._createTestResult(test, r));
  333. return {
  334. testCase: {
  335. testId: test.id,
  336. title: test.title,
  337. projectName,
  338. botName,
  339. location,
  340. duration,
  341. annotations: test.annotations,
  342. outcome: test.outcome(),
  343. path,
  344. results,
  345. ok: test.outcome() === 'expected' || test.outcome() === 'flaky'
  346. },
  347. testCaseSummary: {
  348. testId: test.id,
  349. title: test.title,
  350. projectName,
  351. botName,
  352. location,
  353. duration,
  354. annotations: test.annotations,
  355. outcome: test.outcome(),
  356. path,
  357. ok: test.outcome() === 'expected' || test.outcome() === 'flaky',
  358. results: results.map(result => {
  359. return {
  360. attachments: result.attachments.map(a => ({
  361. name: a.name,
  362. contentType: a.contentType,
  363. path: a.path
  364. }))
  365. };
  366. })
  367. }
  368. };
  369. }
  370. _serializeAttachments(attachments) {
  371. let lastAttachment;
  372. return attachments.map(a => {
  373. if (a.name === 'trace') this._hasTraces = true;
  374. if ((a.name === 'stdout' || a.name === 'stderr') && a.contentType === 'text/plain') {
  375. if (lastAttachment && lastAttachment.name === a.name && lastAttachment.contentType === a.contentType) {
  376. lastAttachment.body += (0, _base.stripAnsiEscapes)(a.body);
  377. return null;
  378. }
  379. a.body = (0, _base.stripAnsiEscapes)(a.body);
  380. lastAttachment = a;
  381. return a;
  382. }
  383. if (a.path) {
  384. let fileName = a.path;
  385. try {
  386. const buffer = _fs.default.readFileSync(a.path);
  387. const sha1 = (0, _utils.calculateSha1)(buffer) + _path.default.extname(a.path);
  388. fileName = this._attachmentsBaseURL + sha1;
  389. _fs.default.mkdirSync(_path.default.join(this._reportFolder, 'data'), {
  390. recursive: true
  391. });
  392. _fs.default.writeFileSync(_path.default.join(this._reportFolder, 'data', sha1), buffer);
  393. } catch (e) {}
  394. return {
  395. name: a.name,
  396. contentType: a.contentType,
  397. path: fileName,
  398. body: a.body
  399. };
  400. }
  401. if (a.body instanceof Buffer) {
  402. if (isTextContentType(a.contentType)) {
  403. var _a$contentType$match;
  404. // Content type is like this: "text/html; charset=UTF-8"
  405. const charset = (_a$contentType$match = a.contentType.match(/charset=(.*)/)) === null || _a$contentType$match === void 0 ? void 0 : _a$contentType$match[1];
  406. try {
  407. const body = a.body.toString(charset || 'utf-8');
  408. return {
  409. name: a.name,
  410. contentType: a.contentType,
  411. body
  412. };
  413. } catch (e) {
  414. // Invalid encoding, fall through and save to file.
  415. }
  416. }
  417. _fs.default.mkdirSync(_path.default.join(this._reportFolder, 'data'), {
  418. recursive: true
  419. });
  420. const extension = (0, _utils.sanitizeForFilePath)(_path.default.extname(a.name).replace(/^\./, '')) || _utilsBundle.mime.getExtension(a.contentType) || 'dat';
  421. const sha1 = (0, _utils.calculateSha1)(a.body) + '.' + extension;
  422. _fs.default.writeFileSync(_path.default.join(this._reportFolder, 'data', sha1), a.body);
  423. return {
  424. name: a.name,
  425. contentType: a.contentType,
  426. path: this._attachmentsBaseURL + sha1
  427. };
  428. }
  429. // string
  430. return {
  431. name: a.name,
  432. contentType: a.contentType,
  433. body: a.body
  434. };
  435. }).filter(Boolean);
  436. }
  437. _createTestResult(test, result) {
  438. return {
  439. duration: result.duration,
  440. startTime: result.startTime.toISOString(),
  441. retry: result.retry,
  442. steps: dedupeSteps(result.steps).map(s => this._createTestStep(s)),
  443. errors: (0, _base.formatResultFailure)(test, result, '', true).map(error => error.message),
  444. status: result.status,
  445. attachments: this._serializeAttachments([...result.attachments, ...result.stdout.map(m => stdioAttachment(m, 'stdout')), ...result.stderr.map(m => stdioAttachment(m, 'stderr'))])
  446. };
  447. }
  448. _createTestStep(dedupedStep) {
  449. var _step$error;
  450. const {
  451. step,
  452. duration,
  453. count
  454. } = dedupedStep;
  455. const result = {
  456. title: step.title,
  457. startTime: step.startTime.toISOString(),
  458. duration,
  459. steps: dedupeSteps(step.steps).map(s => this._createTestStep(s)),
  460. location: this._relativeLocation(step.location),
  461. error: (_step$error = step.error) === null || _step$error === void 0 ? void 0 : _step$error.message,
  462. count
  463. };
  464. if (result.location) this._stepsInFile.set(result.location.file, result);
  465. return result;
  466. }
  467. _relativeLocation(location) {
  468. if (!location) return undefined;
  469. const file = (0, _json.toPosixPath)(_path.default.relative(this._config.rootDir, location.file));
  470. return {
  471. file,
  472. line: location.line,
  473. column: location.column
  474. };
  475. }
  476. }
  477. const emptyStats = () => {
  478. return {
  479. total: 0,
  480. expected: 0,
  481. unexpected: 0,
  482. flaky: 0,
  483. skipped: 0,
  484. ok: true
  485. };
  486. };
  487. const addStats = (stats, delta) => {
  488. stats.total += delta.total;
  489. stats.skipped += delta.skipped;
  490. stats.expected += delta.expected;
  491. stats.unexpected += delta.unexpected;
  492. stats.flaky += delta.flaky;
  493. stats.ok = stats.ok && delta.ok;
  494. return stats;
  495. };
  496. class Base64Encoder extends _stream.Transform {
  497. constructor(...args) {
  498. super(...args);
  499. this._remainder = void 0;
  500. }
  501. _transform(chunk, encoding, callback) {
  502. if (this._remainder) {
  503. chunk = Buffer.concat([this._remainder, chunk]);
  504. this._remainder = undefined;
  505. }
  506. const remaining = chunk.length % 3;
  507. if (remaining) {
  508. this._remainder = chunk.slice(chunk.length - remaining);
  509. chunk = chunk.slice(0, chunk.length - remaining);
  510. }
  511. chunk = chunk.toString('base64');
  512. this.push(Buffer.from(chunk));
  513. callback();
  514. }
  515. _flush(callback) {
  516. if (this._remainder) this.push(Buffer.from(this._remainder.toString('base64')));
  517. callback();
  518. }
  519. }
  520. function isTextContentType(contentType) {
  521. return contentType.startsWith('text/') || contentType.startsWith('application/json');
  522. }
  523. function stdioAttachment(chunk, type) {
  524. if (typeof chunk === 'string') {
  525. return {
  526. name: type,
  527. contentType: 'text/plain',
  528. body: chunk
  529. };
  530. }
  531. return {
  532. name: type,
  533. contentType: 'application/octet-stream',
  534. body: chunk
  535. };
  536. }
  537. function dedupeSteps(steps) {
  538. const result = [];
  539. let lastResult = undefined;
  540. for (const step of steps) {
  541. var _step$location, _lastResult, _step$location2, _lastStep$location, _step$location3, _lastStep$location2, _step$location4, _lastStep$location3;
  542. const canDedupe = !step.error && step.duration >= 0 && ((_step$location = step.location) === null || _step$location === void 0 ? void 0 : _step$location.file) && !step.steps.length;
  543. const lastStep = (_lastResult = lastResult) === null || _lastResult === void 0 ? void 0 : _lastResult.step;
  544. if (canDedupe && lastResult && lastStep && step.category === lastStep.category && step.title === lastStep.title && ((_step$location2 = step.location) === null || _step$location2 === void 0 ? void 0 : _step$location2.file) === ((_lastStep$location = lastStep.location) === null || _lastStep$location === void 0 ? void 0 : _lastStep$location.file) && ((_step$location3 = step.location) === null || _step$location3 === void 0 ? void 0 : _step$location3.line) === ((_lastStep$location2 = lastStep.location) === null || _lastStep$location2 === void 0 ? void 0 : _lastStep$location2.line) && ((_step$location4 = step.location) === null || _step$location4 === void 0 ? void 0 : _step$location4.column) === ((_lastStep$location3 = lastStep.location) === null || _lastStep$location3 === void 0 ? void 0 : _lastStep$location3.column)) {
  545. ++lastResult.count;
  546. lastResult.duration += step.duration;
  547. continue;
  548. }
  549. lastResult = {
  550. step,
  551. count: 1,
  552. duration: step.duration
  553. };
  554. result.push(lastResult);
  555. if (!canDedupe) lastResult = undefined;
  556. }
  557. return result;
  558. }
  559. function createSnippets(stepsInFile) {
  560. for (const file of stepsInFile.keys()) {
  561. let source;
  562. try {
  563. source = _fs.default.readFileSync(file, 'utf-8') + '\n//';
  564. } catch (e) {
  565. continue;
  566. }
  567. const lines = source.split('\n').length;
  568. const highlighted = (0, _babelBundle.codeFrameColumns)(source, {
  569. start: {
  570. line: lines,
  571. column: 1
  572. }
  573. }, {
  574. highlightCode: true,
  575. linesAbove: lines,
  576. linesBelow: 0
  577. });
  578. const highlightedLines = highlighted.split('\n');
  579. const lineWithArrow = highlightedLines[highlightedLines.length - 1];
  580. for (const step of stepsInFile.get(file)) {
  581. // Don't bother with snippets that have less than 3 lines.
  582. if (step.location.line < 2 || step.location.line >= lines) continue;
  583. // Cut out snippet.
  584. const snippetLines = highlightedLines.slice(step.location.line - 2, step.location.line + 1);
  585. // Relocate arrow.
  586. const index = lineWithArrow.indexOf('^');
  587. const shiftedArrow = lineWithArrow.slice(0, index) + ' '.repeat(step.location.column - 1) + lineWithArrow.slice(index);
  588. // Insert arrow line.
  589. snippetLines.splice(2, 0, shiftedArrow);
  590. step.snippet = snippetLines.join('\n');
  591. }
  592. }
  593. }
  594. var _default = exports.default = HtmlReporter;