|
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
|
}); |
|
346
|
|