testTracing.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345
  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 testTracing_exports = {};
  30. __export(testTracing_exports, {
  31. TestTracing: () => TestTracing,
  32. testTraceEntryName: () => testTraceEntryName
  33. });
  34. module.exports = __toCommonJS(testTracing_exports);
  35. var import_fs = __toESM(require("fs"));
  36. var import_path = __toESM(require("path"));
  37. var import_utils = require("playwright-core/lib/utils");
  38. var import_zipBundle = require("playwright-core/lib/zipBundle");
  39. var import_util = require("../util");
  40. const testTraceEntryName = "test.trace";
  41. const version = 8;
  42. let traceOrdinal = 0;
  43. class TestTracing {
  44. constructor(testInfo, artifactsDir) {
  45. this._traceEvents = [];
  46. this._temporaryTraceFiles = [];
  47. this._didFinishTestFunctionAndAfterEachHooks = false;
  48. this._testInfo = testInfo;
  49. this._artifactsDir = artifactsDir;
  50. this._tracesDir = import_path.default.join(this._artifactsDir, "traces");
  51. this._contextCreatedEvent = {
  52. version,
  53. type: "context-options",
  54. origin: "testRunner",
  55. browserName: "",
  56. playwrightVersion: (0, import_utils.getPlaywrightVersion)(),
  57. options: {},
  58. platform: process.platform,
  59. wallTime: Date.now(),
  60. monotonicTime: (0, import_utils.monotonicTime)(),
  61. sdkLanguage: "javascript"
  62. };
  63. this._appendTraceEvent(this._contextCreatedEvent);
  64. }
  65. _shouldCaptureTrace() {
  66. if (this._options?.mode === "on")
  67. return true;
  68. if (this._options?.mode === "retain-on-failure")
  69. return true;
  70. if (this._options?.mode === "on-first-retry" && this._testInfo.retry === 1)
  71. return true;
  72. if (this._options?.mode === "on-all-retries" && this._testInfo.retry > 0)
  73. return true;
  74. if (this._options?.mode === "retain-on-first-failure" && this._testInfo.retry === 0)
  75. return true;
  76. return false;
  77. }
  78. async startIfNeeded(value) {
  79. const defaultTraceOptions = { screenshots: true, snapshots: true, sources: true, attachments: true, _live: false, mode: "off" };
  80. if (!value) {
  81. this._options = defaultTraceOptions;
  82. } else if (typeof value === "string") {
  83. this._options = { ...defaultTraceOptions, mode: value === "retry-with-trace" ? "on-first-retry" : value };
  84. } else {
  85. const mode = value.mode || "off";
  86. this._options = { ...defaultTraceOptions, ...value, mode: mode === "retry-with-trace" ? "on-first-retry" : mode };
  87. }
  88. if (!this._shouldCaptureTrace()) {
  89. this._options = void 0;
  90. return;
  91. }
  92. if (!this._liveTraceFile && this._options._live) {
  93. this._liveTraceFile = { file: import_path.default.join(this._tracesDir, `${this._testInfo.testId}-test.trace`), fs: new import_utils.SerializedFS() };
  94. this._liveTraceFile.fs.mkdir(import_path.default.dirname(this._liveTraceFile.file));
  95. const data = this._traceEvents.map((e) => JSON.stringify(e)).join("\n") + "\n";
  96. this._liveTraceFile.fs.writeFile(this._liveTraceFile.file, data);
  97. }
  98. }
  99. didFinishTestFunctionAndAfterEachHooks() {
  100. this._didFinishTestFunctionAndAfterEachHooks = true;
  101. }
  102. artifactsDir() {
  103. return this._artifactsDir;
  104. }
  105. tracesDir() {
  106. return this._tracesDir;
  107. }
  108. traceTitle() {
  109. return [import_path.default.relative(this._testInfo.project.testDir, this._testInfo.file) + ":" + this._testInfo.line, ...this._testInfo.titlePath.slice(1)].join(" \u203A ");
  110. }
  111. generateNextTraceRecordingName() {
  112. const ordinalSuffix = traceOrdinal ? `-recording${traceOrdinal}` : "";
  113. ++traceOrdinal;
  114. const retrySuffix = this._testInfo.retry ? `-retry${this._testInfo.retry}` : "";
  115. return `${this._testInfo.testId}${retrySuffix}${ordinalSuffix}`;
  116. }
  117. _generateNextTraceRecordingPath() {
  118. const file = import_path.default.join(this._artifactsDir, (0, import_utils.createGuid)() + ".zip");
  119. this._temporaryTraceFiles.push(file);
  120. return file;
  121. }
  122. traceOptions() {
  123. return this._options;
  124. }
  125. maybeGenerateNextTraceRecordingPath() {
  126. if (this._didFinishTestFunctionAndAfterEachHooks && this._shouldAbandonTrace())
  127. return;
  128. return this._generateNextTraceRecordingPath();
  129. }
  130. _shouldAbandonTrace() {
  131. if (!this._options)
  132. return true;
  133. const testFailed = this._testInfo.status !== this._testInfo.expectedStatus;
  134. return !testFailed && (this._options.mode === "retain-on-failure" || this._options.mode === "retain-on-first-failure");
  135. }
  136. async stopIfNeeded() {
  137. if (!this._options)
  138. return;
  139. const error = await this._liveTraceFile?.fs.syncAndGetError();
  140. if (error)
  141. throw error;
  142. if (this._shouldAbandonTrace()) {
  143. for (const file of this._temporaryTraceFiles)
  144. await import_fs.default.promises.unlink(file).catch(() => {
  145. });
  146. return;
  147. }
  148. const zipFile = new import_zipBundle.yazl.ZipFile();
  149. if (!this._options?.attachments) {
  150. for (const event of this._traceEvents) {
  151. if (event.type === "after")
  152. delete event.attachments;
  153. }
  154. }
  155. if (this._options?.sources) {
  156. const sourceFiles = /* @__PURE__ */ new Set();
  157. for (const event of this._traceEvents) {
  158. if (event.type === "before") {
  159. for (const frame of event.stack || [])
  160. sourceFiles.add(frame.file);
  161. }
  162. }
  163. for (const sourceFile of sourceFiles) {
  164. await import_fs.default.promises.readFile(sourceFile, "utf8").then((source) => {
  165. zipFile.addBuffer(Buffer.from(source), "resources/src@" + (0, import_utils.calculateSha1)(sourceFile) + ".txt");
  166. }).catch(() => {
  167. });
  168. }
  169. }
  170. const sha1s = /* @__PURE__ */ new Set();
  171. for (const event of this._traceEvents.filter((e) => e.type === "after")) {
  172. for (const attachment of event.attachments || []) {
  173. let contentPromise;
  174. if (attachment.path)
  175. contentPromise = import_fs.default.promises.readFile(attachment.path).catch(() => void 0);
  176. else if (attachment.base64)
  177. contentPromise = Promise.resolve(Buffer.from(attachment.base64, "base64"));
  178. const content = await contentPromise;
  179. if (content === void 0)
  180. continue;
  181. const sha1 = (0, import_utils.calculateSha1)(content);
  182. attachment.sha1 = sha1;
  183. delete attachment.path;
  184. delete attachment.base64;
  185. if (sha1s.has(sha1))
  186. continue;
  187. sha1s.add(sha1);
  188. zipFile.addBuffer(content, "resources/" + sha1);
  189. }
  190. }
  191. const traceContent = Buffer.from(this._traceEvents.map((e) => JSON.stringify(e)).join("\n"));
  192. zipFile.addBuffer(traceContent, testTraceEntryName);
  193. await new Promise((f) => {
  194. zipFile.end(void 0, () => {
  195. zipFile.outputStream.pipe(import_fs.default.createWriteStream(this._generateNextTraceRecordingPath())).on("close", f);
  196. });
  197. });
  198. const tracePath = this._testInfo.outputPath("trace.zip");
  199. await mergeTraceFiles(tracePath, this._temporaryTraceFiles);
  200. this._testInfo.attachments.push({ name: "trace", path: tracePath, contentType: "application/zip" });
  201. }
  202. appendForError(error) {
  203. const rawStack = error.stack?.split("\n") || [];
  204. const stack = rawStack ? (0, import_util.filteredStackTrace)(rawStack) : [];
  205. this._appendTraceEvent({
  206. type: "error",
  207. message: this._formatError(error),
  208. stack
  209. });
  210. }
  211. _formatError(error) {
  212. const parts = [error.message || String(error.value)];
  213. if (error.cause)
  214. parts.push("[cause]: " + this._formatError(error.cause));
  215. return parts.join("\n");
  216. }
  217. appendStdioToTrace(type, chunk) {
  218. this._appendTraceEvent({
  219. type,
  220. timestamp: (0, import_utils.monotonicTime)(),
  221. text: typeof chunk === "string" ? chunk : void 0,
  222. base64: typeof chunk === "string" ? void 0 : chunk.toString("base64")
  223. });
  224. }
  225. appendBeforeActionForStep(options) {
  226. this._appendTraceEvent({
  227. type: "before",
  228. callId: options.stepId,
  229. stepId: options.stepId,
  230. parentId: options.parentId,
  231. startTime: (0, import_utils.monotonicTime)(),
  232. class: "Test",
  233. method: options.category,
  234. title: options.title,
  235. params: Object.fromEntries(Object.entries(options.params || {}).map(([name, value]) => [name, generatePreview(value)])),
  236. stack: options.stack,
  237. group: options.group
  238. });
  239. }
  240. appendAfterActionForStep(callId, error, attachments = [], annotations) {
  241. this._appendTraceEvent({
  242. type: "after",
  243. callId,
  244. endTime: (0, import_utils.monotonicTime)(),
  245. attachments: serializeAttachments(attachments),
  246. annotations,
  247. error
  248. });
  249. }
  250. _appendTraceEvent(event) {
  251. this._traceEvents.push(event);
  252. if (this._liveTraceFile)
  253. this._liveTraceFile.fs.appendFile(this._liveTraceFile.file, JSON.stringify(event) + "\n", true);
  254. }
  255. }
  256. function serializeAttachments(attachments) {
  257. if (attachments.length === 0)
  258. return void 0;
  259. return attachments.filter((a) => a.name !== "trace").map((a) => {
  260. return {
  261. name: a.name,
  262. contentType: a.contentType,
  263. path: a.path,
  264. base64: a.body?.toString("base64")
  265. };
  266. });
  267. }
  268. function generatePreview(value, visited = /* @__PURE__ */ new Set()) {
  269. if (visited.has(value))
  270. return "";
  271. visited.add(value);
  272. if (typeof value === "string")
  273. return value;
  274. if (typeof value === "number")
  275. return value.toString();
  276. if (typeof value === "boolean")
  277. return value.toString();
  278. if (value === null)
  279. return "null";
  280. if (value === void 0)
  281. return "undefined";
  282. if (Array.isArray(value))
  283. return "[" + value.map((v) => generatePreview(v, visited)).join(", ") + "]";
  284. if (typeof value === "object")
  285. return "Object";
  286. return String(value);
  287. }
  288. async function mergeTraceFiles(fileName, temporaryTraceFiles) {
  289. temporaryTraceFiles = temporaryTraceFiles.filter((file) => import_fs.default.existsSync(file));
  290. if (temporaryTraceFiles.length === 1) {
  291. await import_fs.default.promises.rename(temporaryTraceFiles[0], fileName);
  292. return;
  293. }
  294. const mergePromise = new import_utils.ManualPromise();
  295. const zipFile = new import_zipBundle.yazl.ZipFile();
  296. const entryNames = /* @__PURE__ */ new Set();
  297. zipFile.on("error", (error) => mergePromise.reject(error));
  298. for (let i = temporaryTraceFiles.length - 1; i >= 0; --i) {
  299. const tempFile = temporaryTraceFiles[i];
  300. const promise = new import_utils.ManualPromise();
  301. import_zipBundle.yauzl.open(tempFile, (err, inZipFile) => {
  302. if (err) {
  303. promise.reject(err);
  304. return;
  305. }
  306. let pendingEntries = inZipFile.entryCount;
  307. inZipFile.on("entry", (entry) => {
  308. let entryName = entry.fileName;
  309. if (entry.fileName === testTraceEntryName) {
  310. } else if (entry.fileName.match(/trace\.[a-z]*$/)) {
  311. entryName = i + "-" + entry.fileName;
  312. }
  313. if (entryNames.has(entryName)) {
  314. if (--pendingEntries === 0)
  315. promise.resolve();
  316. return;
  317. }
  318. entryNames.add(entryName);
  319. inZipFile.openReadStream(entry, (err2, readStream) => {
  320. if (err2) {
  321. promise.reject(err2);
  322. return;
  323. }
  324. zipFile.addReadStream(readStream, entryName);
  325. if (--pendingEntries === 0)
  326. promise.resolve();
  327. });
  328. });
  329. });
  330. await promise;
  331. }
  332. zipFile.end(void 0, () => {
  333. zipFile.outputStream.pipe(import_fs.default.createWriteStream(fileName)).on("close", () => {
  334. void Promise.all(temporaryTraceFiles.map((tempFile) => import_fs.default.promises.unlink(tempFile))).then(() => {
  335. mergePromise.resolve();
  336. }).catch((error) => mergePromise.reject(error));
  337. }).on("error", (error) => mergePromise.reject(error));
  338. });
  339. await mergePromise;
  340. }
  341. // Annotate the CommonJS export names for ESM import in node:
  342. 0 && (module.exports = {
  343. TestTracing,
  344. testTraceEntryName
  345. });