junit.js 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230
  1. "use strict";
  2. var __create = Object.create;
  3. var __defProp = Object.defineProperty;
  4. var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
  5. var __getOwnPropNames = Object.getOwnPropertyNames;
  6. var __getProtoOf = Object.getPrototypeOf;
  7. var __hasOwnProp = Object.prototype.hasOwnProperty;
  8. var __export = (target, all) => {
  9. for (var name in all)
  10. __defProp(target, name, { get: all[name], enumerable: true });
  11. };
  12. var __copyProps = (to, from, except, desc) => {
  13. if (from && typeof from === "object" || typeof from === "function") {
  14. for (let key of __getOwnPropNames(from))
  15. if (!__hasOwnProp.call(to, key) && key !== except)
  16. __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
  17. }
  18. return to;
  19. };
  20. var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
  21. // If the importer is in node compatibility mode or this is not an ESM
  22. // file that has been converted to a CommonJS file using a Babel-
  23. // compatible transform (i.e. "__esModule" has not been set), then set
  24. // "default" to the CommonJS "module.exports" for node compatibility.
  25. isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
  26. mod
  27. ));
  28. var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
  29. var junit_exports = {};
  30. __export(junit_exports, {
  31. default: () => junit_default
  32. });
  33. module.exports = __toCommonJS(junit_exports);
  34. var import_fs = __toESM(require("fs"));
  35. var import_path = __toESM(require("path"));
  36. var import_utils = require("playwright-core/lib/utils");
  37. var import_base = require("./base");
  38. var import_util = require("../util");
  39. class JUnitReporter {
  40. constructor(options) {
  41. this.totalTests = 0;
  42. this.totalFailures = 0;
  43. this.totalSkipped = 0;
  44. this.stripANSIControlSequences = false;
  45. this.includeProjectInTestName = false;
  46. this.stripANSIControlSequences = (0, import_utils.getAsBooleanFromENV)("PLAYWRIGHT_JUNIT_STRIP_ANSI", !!options.stripANSIControlSequences);
  47. this.includeProjectInTestName = (0, import_utils.getAsBooleanFromENV)("PLAYWRIGHT_JUNIT_INCLUDE_PROJECT_IN_TEST_NAME", !!options.includeProjectInTestName);
  48. this.configDir = options.configDir;
  49. this.resolvedOutputFile = (0, import_base.resolveOutputFile)("JUNIT", options)?.outputFile;
  50. }
  51. version() {
  52. return "v2";
  53. }
  54. printsToStdio() {
  55. return !this.resolvedOutputFile;
  56. }
  57. onConfigure(config) {
  58. this.config = config;
  59. }
  60. onBegin(suite) {
  61. this.suite = suite;
  62. this.timestamp = /* @__PURE__ */ new Date();
  63. }
  64. async onEnd(result) {
  65. const children = [];
  66. for (const projectSuite of this.suite.suites) {
  67. for (const fileSuite of projectSuite.suites)
  68. children.push(await this._buildTestSuite(projectSuite.title, fileSuite));
  69. }
  70. const tokens = [];
  71. const self = this;
  72. const root = {
  73. name: "testsuites",
  74. attributes: {
  75. id: process.env[`PLAYWRIGHT_JUNIT_SUITE_ID`] || "",
  76. name: process.env[`PLAYWRIGHT_JUNIT_SUITE_NAME`] || "",
  77. tests: self.totalTests,
  78. failures: self.totalFailures,
  79. skipped: self.totalSkipped,
  80. errors: 0,
  81. time: result.duration / 1e3
  82. },
  83. children
  84. };
  85. serializeXML(root, tokens, this.stripANSIControlSequences);
  86. const reportString = tokens.join("\n");
  87. if (this.resolvedOutputFile) {
  88. await import_fs.default.promises.mkdir(import_path.default.dirname(this.resolvedOutputFile), { recursive: true });
  89. await import_fs.default.promises.writeFile(this.resolvedOutputFile, reportString);
  90. } else {
  91. console.log(reportString);
  92. }
  93. }
  94. async _buildTestSuite(projectName, suite) {
  95. let tests = 0;
  96. let skipped = 0;
  97. let failures = 0;
  98. let duration = 0;
  99. const children = [];
  100. const testCaseNamePrefix = projectName && this.includeProjectInTestName ? `[${projectName}] ` : "";
  101. for (const test of suite.allTests()) {
  102. ++tests;
  103. if (test.outcome() === "skipped")
  104. ++skipped;
  105. if (!test.ok())
  106. ++failures;
  107. for (const result of test.results)
  108. duration += result.duration;
  109. await this._addTestCase(suite.title, testCaseNamePrefix, test, children);
  110. }
  111. this.totalTests += tests;
  112. this.totalSkipped += skipped;
  113. this.totalFailures += failures;
  114. const entry = {
  115. name: "testsuite",
  116. attributes: {
  117. name: suite.title,
  118. timestamp: this.timestamp.toISOString(),
  119. hostname: projectName,
  120. tests,
  121. failures,
  122. skipped,
  123. time: duration / 1e3,
  124. errors: 0
  125. },
  126. children
  127. };
  128. return entry;
  129. }
  130. async _addTestCase(suiteName, namePrefix, test, entries) {
  131. const entry = {
  132. name: "testcase",
  133. attributes: {
  134. // Skip root, project, file
  135. name: namePrefix + test.titlePath().slice(3).join(" \u203A "),
  136. // filename
  137. classname: suiteName,
  138. time: test.results.reduce((acc, value) => acc + value.duration, 0) / 1e3
  139. },
  140. children: []
  141. };
  142. entries.push(entry);
  143. const properties = {
  144. name: "properties",
  145. children: []
  146. };
  147. for (const annotation of test.annotations) {
  148. const property = {
  149. name: "property",
  150. attributes: {
  151. name: annotation.type,
  152. value: annotation?.description ? annotation.description : ""
  153. }
  154. };
  155. properties.children?.push(property);
  156. }
  157. if (properties.children?.length)
  158. entry.children.push(properties);
  159. if (test.outcome() === "skipped") {
  160. entry.children.push({ name: "skipped" });
  161. return;
  162. }
  163. if (!test.ok()) {
  164. entry.children.push({
  165. name: "failure",
  166. attributes: {
  167. message: `${import_path.default.basename(test.location.file)}:${test.location.line}:${test.location.column} ${test.title}`,
  168. type: "FAILURE"
  169. },
  170. text: (0, import_util.stripAnsiEscapes)((0, import_base.formatFailure)(import_base.nonTerminalScreen, this.config, test))
  171. });
  172. }
  173. const systemOut = [];
  174. const systemErr = [];
  175. for (const result of test.results) {
  176. systemOut.push(...result.stdout.map((item) => item.toString()));
  177. systemErr.push(...result.stderr.map((item) => item.toString()));
  178. for (const attachment of result.attachments) {
  179. if (!attachment.path)
  180. continue;
  181. let attachmentPath = import_path.default.relative(this.configDir, attachment.path);
  182. try {
  183. if (this.resolvedOutputFile)
  184. attachmentPath = import_path.default.relative(import_path.default.dirname(this.resolvedOutputFile), attachment.path);
  185. } catch {
  186. systemOut.push(`
  187. Warning: Unable to make attachment path ${attachment.path} relative to report output file ${this.resolvedOutputFile}`);
  188. }
  189. try {
  190. await import_fs.default.promises.access(attachment.path);
  191. systemOut.push(`
  192. [[ATTACHMENT|${attachmentPath}]]
  193. `);
  194. } catch {
  195. systemErr.push(`
  196. Warning: attachment ${attachmentPath} is missing`);
  197. }
  198. }
  199. }
  200. if (systemOut.length)
  201. entry.children.push({ name: "system-out", text: systemOut.join("") });
  202. if (systemErr.length)
  203. entry.children.push({ name: "system-err", text: systemErr.join("") });
  204. }
  205. }
  206. function serializeXML(entry, tokens, stripANSIControlSequences) {
  207. const attrs = [];
  208. for (const [name, value] of Object.entries(entry.attributes || {}))
  209. attrs.push(`${name}="${escape(String(value), stripANSIControlSequences, false)}"`);
  210. tokens.push(`<${entry.name}${attrs.length ? " " : ""}${attrs.join(" ")}>`);
  211. for (const child of entry.children || [])
  212. serializeXML(child, tokens, stripANSIControlSequences);
  213. if (entry.text)
  214. tokens.push(escape(entry.text, stripANSIControlSequences, true));
  215. tokens.push(`</${entry.name}>`);
  216. }
  217. const discouragedXMLCharacters = /[\u0000-\u0008\u000b-\u000c\u000e-\u001f\u007f-\u0084\u0086-\u009f]/g;
  218. function escape(text, stripANSIControlSequences, isCharacterData) {
  219. if (stripANSIControlSequences)
  220. text = (0, import_util.stripAnsiEscapes)(text);
  221. if (isCharacterData) {
  222. text = "<![CDATA[" + text.replace(/]]>/g, "]]&gt;") + "]]>";
  223. } else {
  224. const escapeRe = /[&"'<>]/g;
  225. text = text.replace(escapeRe, (c) => ({ "&": "&amp;", '"': "&quot;", "'": "&apos;", "<": "&lt;", ">": "&gt;" })[c]);
  226. }
  227. text = text.replace(discouragedXMLCharacters, "");
  228. return text;
  229. }
  230. var junit_default = JUnitReporter;