codeceptjs 3.7.3 → 3.7.5-beta.1

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.
@@ -0,0 +1,327 @@
1
+ const assert = require('assert')
2
+
3
+ /**
4
+ * Unified WebElement class that wraps native element instances from different helpers
5
+ * and provides a consistent API across all supported helpers (Playwright, WebDriver, Puppeteer).
6
+ */
7
+ class WebElement {
8
+ constructor(element, helper) {
9
+ this.element = element
10
+ this.helper = helper
11
+ this.helperType = this._detectHelperType(helper)
12
+ }
13
+
14
+ _detectHelperType(helper) {
15
+ if (!helper) return 'unknown'
16
+
17
+ const className = helper.constructor.name
18
+ if (className === 'Playwright') return 'playwright'
19
+ if (className === 'WebDriver') return 'webdriver'
20
+ if (className === 'Puppeteer') return 'puppeteer'
21
+
22
+ return 'unknown'
23
+ }
24
+
25
+ /**
26
+ * Get the native element instance
27
+ * @returns {ElementHandle|WebElement|ElementHandle} Native element
28
+ */
29
+ getNativeElement() {
30
+ return this.element
31
+ }
32
+
33
+ /**
34
+ * Get the helper instance
35
+ * @returns {Helper} Helper instance
36
+ */
37
+ getHelper() {
38
+ return this.helper
39
+ }
40
+
41
+ /**
42
+ * Get text content of the element
43
+ * @returns {Promise<string>} Element text content
44
+ */
45
+ async getText() {
46
+ switch (this.helperType) {
47
+ case 'playwright':
48
+ return this.element.textContent()
49
+ case 'webdriver':
50
+ return this.element.getText()
51
+ case 'puppeteer':
52
+ return this.element.evaluate(el => el.textContent)
53
+ default:
54
+ throw new Error(`Unsupported helper type: ${this.helperType}`)
55
+ }
56
+ }
57
+
58
+ /**
59
+ * Get attribute value of the element
60
+ * @param {string} name Attribute name
61
+ * @returns {Promise<string|null>} Attribute value
62
+ */
63
+ async getAttribute(name) {
64
+ switch (this.helperType) {
65
+ case 'playwright':
66
+ return this.element.getAttribute(name)
67
+ case 'webdriver':
68
+ return this.element.getAttribute(name)
69
+ case 'puppeteer':
70
+ return this.element.evaluate((el, attrName) => el.getAttribute(attrName), name)
71
+ default:
72
+ throw new Error(`Unsupported helper type: ${this.helperType}`)
73
+ }
74
+ }
75
+
76
+ /**
77
+ * Get property value of the element
78
+ * @param {string} name Property name
79
+ * @returns {Promise<any>} Property value
80
+ */
81
+ async getProperty(name) {
82
+ switch (this.helperType) {
83
+ case 'playwright':
84
+ return this.element.evaluate((el, propName) => el[propName], name)
85
+ case 'webdriver':
86
+ return this.element.getProperty(name)
87
+ case 'puppeteer':
88
+ return this.element.evaluate((el, propName) => el[propName], name)
89
+ default:
90
+ throw new Error(`Unsupported helper type: ${this.helperType}`)
91
+ }
92
+ }
93
+
94
+ /**
95
+ * Get innerHTML of the element
96
+ * @returns {Promise<string>} Element innerHTML
97
+ */
98
+ async getInnerHTML() {
99
+ switch (this.helperType) {
100
+ case 'playwright':
101
+ return this.element.innerHTML()
102
+ case 'webdriver':
103
+ return this.element.getProperty('innerHTML')
104
+ case 'puppeteer':
105
+ return this.element.evaluate(el => el.innerHTML)
106
+ default:
107
+ throw new Error(`Unsupported helper type: ${this.helperType}`)
108
+ }
109
+ }
110
+
111
+ /**
112
+ * Get value of the element (for input elements)
113
+ * @returns {Promise<string>} Element value
114
+ */
115
+ async getValue() {
116
+ switch (this.helperType) {
117
+ case 'playwright':
118
+ return this.element.inputValue()
119
+ case 'webdriver':
120
+ return this.element.getValue()
121
+ case 'puppeteer':
122
+ return this.element.evaluate(el => el.value)
123
+ default:
124
+ throw new Error(`Unsupported helper type: ${this.helperType}`)
125
+ }
126
+ }
127
+
128
+ /**
129
+ * Check if element is visible
130
+ * @returns {Promise<boolean>} True if element is visible
131
+ */
132
+ async isVisible() {
133
+ switch (this.helperType) {
134
+ case 'playwright':
135
+ return this.element.isVisible()
136
+ case 'webdriver':
137
+ return this.element.isDisplayed()
138
+ case 'puppeteer':
139
+ return this.element.evaluate(el => {
140
+ const style = window.getComputedStyle(el)
141
+ return style.display !== 'none' && style.visibility !== 'hidden' && style.opacity !== '0'
142
+ })
143
+ default:
144
+ throw new Error(`Unsupported helper type: ${this.helperType}`)
145
+ }
146
+ }
147
+
148
+ /**
149
+ * Check if element is enabled
150
+ * @returns {Promise<boolean>} True if element is enabled
151
+ */
152
+ async isEnabled() {
153
+ switch (this.helperType) {
154
+ case 'playwright':
155
+ return this.element.isEnabled()
156
+ case 'webdriver':
157
+ return this.element.isEnabled()
158
+ case 'puppeteer':
159
+ return this.element.evaluate(el => !el.disabled)
160
+ default:
161
+ throw new Error(`Unsupported helper type: ${this.helperType}`)
162
+ }
163
+ }
164
+
165
+ /**
166
+ * Check if element exists in DOM
167
+ * @returns {Promise<boolean>} True if element exists
168
+ */
169
+ async exists() {
170
+ try {
171
+ switch (this.helperType) {
172
+ case 'playwright':
173
+ // For Playwright, if we have the element, it exists
174
+ return await this.element.evaluate(el => !!el)
175
+ case 'webdriver':
176
+ // For WebDriver, if we have the element, it exists
177
+ return true
178
+ case 'puppeteer':
179
+ // For Puppeteer, if we have the element, it exists
180
+ return await this.element.evaluate(el => !!el)
181
+ default:
182
+ throw new Error(`Unsupported helper type: ${this.helperType}`)
183
+ }
184
+ } catch (e) {
185
+ return false
186
+ }
187
+ }
188
+
189
+ /**
190
+ * Get bounding box of the element
191
+ * @returns {Promise<Object>} Bounding box with x, y, width, height properties
192
+ */
193
+ async getBoundingBox() {
194
+ switch (this.helperType) {
195
+ case 'playwright':
196
+ return this.element.boundingBox()
197
+ case 'webdriver':
198
+ const rect = await this.element.getRect()
199
+ return {
200
+ x: rect.x,
201
+ y: rect.y,
202
+ width: rect.width,
203
+ height: rect.height,
204
+ }
205
+ case 'puppeteer':
206
+ return this.element.boundingBox()
207
+ default:
208
+ throw new Error(`Unsupported helper type: ${this.helperType}`)
209
+ }
210
+ }
211
+
212
+ /**
213
+ * Click the element
214
+ * @param {Object} options Click options
215
+ * @returns {Promise<void>}
216
+ */
217
+ async click(options = {}) {
218
+ switch (this.helperType) {
219
+ case 'playwright':
220
+ return this.element.click(options)
221
+ case 'webdriver':
222
+ return this.element.click()
223
+ case 'puppeteer':
224
+ return this.element.click(options)
225
+ default:
226
+ throw new Error(`Unsupported helper type: ${this.helperType}`)
227
+ }
228
+ }
229
+
230
+ /**
231
+ * Type text into the element
232
+ * @param {string} text Text to type
233
+ * @param {Object} options Type options
234
+ * @returns {Promise<void>}
235
+ */
236
+ async type(text, options = {}) {
237
+ switch (this.helperType) {
238
+ case 'playwright':
239
+ return this.element.type(text, options)
240
+ case 'webdriver':
241
+ return this.element.setValue(text)
242
+ case 'puppeteer':
243
+ return this.element.type(text, options)
244
+ default:
245
+ throw new Error(`Unsupported helper type: ${this.helperType}`)
246
+ }
247
+ }
248
+
249
+ /**
250
+ * Find first child element matching the locator
251
+ * @param {string|Object} locator Element locator
252
+ * @returns {Promise<WebElement|null>} WebElement instance or null if not found
253
+ */
254
+ async $(locator) {
255
+ let childElement
256
+
257
+ switch (this.helperType) {
258
+ case 'playwright':
259
+ childElement = await this.element.$(this._normalizeLocator(locator))
260
+ break
261
+ case 'webdriver':
262
+ try {
263
+ childElement = await this.element.$(this._normalizeLocator(locator))
264
+ } catch (e) {
265
+ return null
266
+ }
267
+ break
268
+ case 'puppeteer':
269
+ childElement = await this.element.$(this._normalizeLocator(locator))
270
+ break
271
+ default:
272
+ throw new Error(`Unsupported helper type: ${this.helperType}`)
273
+ }
274
+
275
+ return childElement ? new WebElement(childElement, this.helper) : null
276
+ }
277
+
278
+ /**
279
+ * Find all child elements matching the locator
280
+ * @param {string|Object} locator Element locator
281
+ * @returns {Promise<WebElement[]>} Array of WebElement instances
282
+ */
283
+ async $$(locator) {
284
+ let childElements
285
+
286
+ switch (this.helperType) {
287
+ case 'playwright':
288
+ childElements = await this.element.$$(this._normalizeLocator(locator))
289
+ break
290
+ case 'webdriver':
291
+ childElements = await this.element.$$(this._normalizeLocator(locator))
292
+ break
293
+ case 'puppeteer':
294
+ childElements = await this.element.$$(this._normalizeLocator(locator))
295
+ break
296
+ default:
297
+ throw new Error(`Unsupported helper type: ${this.helperType}`)
298
+ }
299
+
300
+ return childElements.map(el => new WebElement(el, this.helper))
301
+ }
302
+
303
+ /**
304
+ * Normalize locator for element search
305
+ * @param {string|Object} locator Locator to normalize
306
+ * @returns {string} Normalized CSS selector
307
+ * @private
308
+ */
309
+ _normalizeLocator(locator) {
310
+ if (typeof locator === 'string') {
311
+ return locator
312
+ }
313
+
314
+ if (typeof locator === 'object') {
315
+ // Handle CodeceptJS locator objects
316
+ if (locator.css) return locator.css
317
+ if (locator.xpath) return locator.xpath
318
+ if (locator.id) return `#${locator.id}`
319
+ if (locator.name) return `[name="${locator.name}"]`
320
+ if (locator.className) return `.${locator.className}`
321
+ }
322
+
323
+ return locator.toString()
324
+ }
325
+ }
326
+
327
+ module.exports = WebElement
@@ -72,10 +72,11 @@ class JSONResponse extends Helper {
72
72
  if (!this.helpers[this.options.requestHelper]) {
73
73
  throw new Error(`Error setting JSONResponse, helper ${this.options.requestHelper} is not enabled in config, helpers: ${Object.keys(this.helpers)}`)
74
74
  }
75
- // connect to REST helper
75
+ const origOnResponse = this.helpers[this.options.requestHelper].config.onResponse;
76
76
  this.helpers[this.options.requestHelper].config.onResponse = response => {
77
- this.response = response
78
- }
77
+ this.response = response;
78
+ if (typeof origOnResponse === 'function') origOnResponse(response);
79
+ };
79
80
  }
80
81
 
81
82
  _before() {
@@ -349,7 +350,25 @@ class JSONResponse extends Helper {
349
350
  for (const key in expected) {
350
351
  assert(key in actual, `Key "${key}" not found in ${JSON.stringify(actual)}`)
351
352
  if (typeof expected[key] === 'object' && expected[key] !== null) {
352
- this._assertContains(actual[key], expected[key])
353
+ if (Array.isArray(expected[key])) {
354
+ // Handle array comparison: each expected element should have a match in actual array
355
+ assert(Array.isArray(actual[key]), `Expected array for key "${key}", but got ${typeof actual[key]}`)
356
+ for (const expectedItem of expected[key]) {
357
+ let found = false
358
+ for (const actualItem of actual[key]) {
359
+ try {
360
+ this._assertContains(actualItem, expectedItem)
361
+ found = true
362
+ break
363
+ } catch (err) {
364
+ continue
365
+ }
366
+ }
367
+ assert(found, `No matching element found in array for ${JSON.stringify(expectedItem)}`)
368
+ }
369
+ } else {
370
+ this._assertContains(actual[key], expected[key])
371
+ }
353
372
  } else {
354
373
  assert.deepStrictEqual(actual[key], expected[key], `Values for key "${key}" don't match`)
355
374
  }
@@ -1,4 +1,3 @@
1
- let addMochawesomeContext
2
1
  let currentTest
3
2
  let currentSuite
4
3
 
@@ -16,7 +15,8 @@ class Mochawesome extends Helper {
16
15
  disableScreenshots: false,
17
16
  }
18
17
 
19
- addMochawesomeContext = require('mochawesome/addContext')
18
+ this._addContext = require('mochawesome/addContext')
19
+
20
20
  this._createConfig(config)
21
21
  }
22
22
 
@@ -37,35 +37,56 @@ class Mochawesome extends Helper {
37
37
  }
38
38
 
39
39
  _test(test) {
40
- currentTest = { test }
40
+ // If this is a retried test, we want to add context to the retried test
41
+ // but also potentially preserve context from the original test
42
+ const originalTest = test.retriedTest && test.retriedTest()
43
+ if (originalTest) {
44
+ // This is a retried test - use the retried test for context
45
+ currentTest = { test }
46
+
47
+ // Optionally copy context from original test if it exists
48
+ // Note: mochawesome context is stored in test.ctx, but we need to be careful
49
+ // not to break the mocha context structure
50
+ } else {
51
+ // Normal test (not a retry)
52
+ currentTest = { test }
53
+ }
41
54
  }
42
55
 
43
56
  _failed(test) {
44
57
  if (this.options.disableScreenshots) return
45
58
  let fileName
46
59
  // Get proper name if we are fail on hook
47
- if (test.ctx.test.type === 'hook') {
60
+ if (test.ctx?.test?.type === 'hook') {
48
61
  currentTest = { test: test.ctx.test }
49
62
  // ignore retries if we are in hook
50
63
  test._retries = -1
51
64
  fileName = clearString(`${test.title}_${currentTest.test.title}`)
52
65
  } else {
53
66
  currentTest = { test }
54
- fileName = `${testToFileName(test)}`
67
+ fileName = testToFileName(test)
55
68
  }
56
69
  if (this.options.uniqueScreenshotNames) {
57
- const uuid = test.uuid || test.ctx.test.uuid
58
- fileName = `${fileName.substring(0, 10)}_${uuid}`
70
+ fileName = testToFileName(test, { unique: true })
59
71
  }
60
72
  if (test._retries < 1 || test._retries === test.retryNum) {
61
73
  fileName = `${fileName}.failed.png`
62
- return addMochawesomeContext(currentTest, fileName)
74
+ return this._addContext(currentTest, fileName)
63
75
  }
64
76
  }
65
77
 
66
78
  addMochawesomeContext(context) {
67
79
  if (currentTest === '') currentTest = { test: currentSuite.ctx.test }
68
- return addMochawesomeContext(currentTest, context)
80
+
81
+ // For retried tests, make sure we're adding context to the current (retried) test
82
+ // not the original test
83
+ let targetTest = currentTest
84
+ if (currentTest.test && currentTest.test.retriedTest && currentTest.test.retriedTest()) {
85
+ // This test has been retried, make sure we're using the current test for context
86
+ targetTest = { test: currentTest.test }
87
+ }
88
+
89
+ return this._addContext(targetTest, context)
69
90
  }
70
91
  }
71
92
 
@@ -33,6 +33,7 @@ const RemoteBrowserConnectionRefused = require('./errors/RemoteBrowserConnection
33
33
  const Popup = require('./extras/Popup')
34
34
  const Console = require('./extras/Console')
35
35
  const { findReact, findVue, findByPlaywrightLocator } = require('./extras/PlaywrightReactVueLocator')
36
+ const WebElement = require('../element/WebElement')
36
37
 
37
38
  let playwright
38
39
  let perfTiming
@@ -1341,7 +1342,8 @@ class Playwright extends Helper {
1341
1342
  *
1342
1343
  */
1343
1344
  async grabWebElements(locator) {
1344
- return this._locate(locator)
1345
+ const elements = await this._locate(locator)
1346
+ return elements.map(element => new WebElement(element, this))
1345
1347
  }
1346
1348
 
1347
1349
  /**
@@ -1349,7 +1351,8 @@ class Playwright extends Helper {
1349
1351
  *
1350
1352
  */
1351
1353
  async grabWebElement(locator) {
1352
- return this._locateElement(locator)
1354
+ const element = await this._locateElement(locator)
1355
+ return new WebElement(element, this)
1353
1356
  }
1354
1357
 
1355
1358
  /**
@@ -2377,15 +2380,19 @@ class Playwright extends Helper {
2377
2380
  if (this.options.recordVideo && this.page && this.page.video()) {
2378
2381
  test.artifacts.video = saveVideoForPage(this.page, `${test.title}.failed`)
2379
2382
  for (const sessionName in this.sessionPages) {
2380
- test.artifacts[`video_${sessionName}`] = saveVideoForPage(this.sessionPages[sessionName], `${test.title}_${sessionName}.failed`)
2383
+ if (sessionName === '') continue
2384
+ test.artifacts[`video_${sessionName}`] = saveVideoForPage(this.sessionPages[sessionName], `${sessionName}_${test.title}.failed`)
2381
2385
  }
2382
2386
  }
2383
2387
 
2384
2388
  if (this.options.trace) {
2385
2389
  test.artifacts.trace = await saveTraceForContext(this.browserContext, `${test.title}.failed`)
2386
2390
  for (const sessionName in this.sessionPages) {
2387
- if (!this.sessionPages[sessionName].context) continue
2388
- test.artifacts[`trace_${sessionName}`] = await saveTraceForContext(this.sessionPages[sessionName].context, `${test.title}_${sessionName}.failed`)
2391
+ if (sessionName === '') continue
2392
+ const sessionPage = this.sessionPages[sessionName]
2393
+ const sessionContext = sessionPage.context()
2394
+ if (!sessionContext || !sessionContext.tracing) continue
2395
+ test.artifacts[`trace_${sessionName}`] = await saveTraceForContext(sessionContext, `${sessionName}_${test.title}.failed`)
2389
2396
  }
2390
2397
  }
2391
2398
 
@@ -2399,7 +2406,8 @@ class Playwright extends Helper {
2399
2406
  if (this.options.keepVideoForPassedTests) {
2400
2407
  test.artifacts.video = saveVideoForPage(this.page, `${test.title}.passed`)
2401
2408
  for (const sessionName of Object.keys(this.sessionPages)) {
2402
- test.artifacts[`video_${sessionName}`] = saveVideoForPage(this.sessionPages[sessionName], `${test.title}_${sessionName}.passed`)
2409
+ if (sessionName === '') continue
2410
+ test.artifacts[`video_${sessionName}`] = saveVideoForPage(this.sessionPages[sessionName], `${sessionName}_${test.title}.passed`)
2403
2411
  }
2404
2412
  } else {
2405
2413
  this.page
@@ -2414,8 +2422,11 @@ class Playwright extends Helper {
2414
2422
  if (this.options.trace) {
2415
2423
  test.artifacts.trace = await saveTraceForContext(this.browserContext, `${test.title}.passed`)
2416
2424
  for (const sessionName in this.sessionPages) {
2417
- if (!this.sessionPages[sessionName].context) continue
2418
- test.artifacts[`trace_${sessionName}`] = await saveTraceForContext(this.sessionPages[sessionName].context, `${test.title}_${sessionName}.passed`)
2425
+ if (sessionName === '') continue
2426
+ const sessionPage = this.sessionPages[sessionName]
2427
+ const sessionContext = sessionPage.context()
2428
+ if (!sessionContext || !sessionContext.tracing) continue
2429
+ test.artifacts[`trace_${sessionName}`] = await saveTraceForContext(sessionContext, `${sessionName}_${test.title}.passed`)
2419
2430
  }
2420
2431
  }
2421
2432
  } else {
@@ -2768,47 +2779,63 @@ class Playwright extends Helper {
2768
2779
  .locator(`${locator.isCustom() ? `${locator.type}=${locator.value}` : locator.simplify()} >> text=${text}`)
2769
2780
  .first()
2770
2781
  .waitFor({ timeout: waitTimeout, state: 'visible' })
2782
+ .catch(e => {
2783
+ throw new Error(errorMessage)
2784
+ })
2771
2785
  }
2772
2786
 
2773
2787
  if (locator.isXPath()) {
2774
- return contextObject.waitForFunction(
2775
- ([locator, text, $XPath]) => {
2776
- eval($XPath)
2777
- const el = $XPath(null, locator)
2778
- if (!el.length) return false
2779
- return el[0].innerText.indexOf(text) > -1
2780
- },
2781
- [locator.value, text, $XPath.toString()],
2782
- { timeout: waitTimeout },
2783
- )
2788
+ return contextObject
2789
+ .waitForFunction(
2790
+ ([locator, text, $XPath]) => {
2791
+ eval($XPath)
2792
+ const el = $XPath(null, locator)
2793
+ if (!el.length) return false
2794
+ return el[0].innerText.indexOf(text) > -1
2795
+ },
2796
+ [locator.value, text, $XPath.toString()],
2797
+ { timeout: waitTimeout },
2798
+ )
2799
+ .catch(e => {
2800
+ throw new Error(errorMessage)
2801
+ })
2784
2802
  }
2785
2803
  } catch (e) {
2786
2804
  throw new Error(`${errorMessage}\n${e.message}`)
2787
2805
  }
2788
2806
  }
2789
2807
 
2808
+ // Based on original implementation but fixed to check title text and remove problematic promiseRetry
2809
+ // Original used timeoutGap for waitForFunction to give it slightly more time than the locator
2790
2810
  const timeoutGap = waitTimeout + 1000
2791
2811
 
2792
- // We add basic timeout to make sure we don't wait forever
2793
- // We apply 2 strategies here: wait for text as innert text on page (wide strategy) - older
2794
- // or we use native Playwright matcher to wait for text in element (narrow strategy) - newer
2795
- // If a user waits for text on a page they are mostly expect it to be there, so wide strategy can be helpful even PW strategy is available
2796
2812
  return Promise.race([
2797
- new Promise((_, reject) => {
2798
- setTimeout(() => reject(errorMessage), waitTimeout)
2799
- }),
2800
- this.page.waitForFunction(text => document.body && document.body.innerText.indexOf(text) > -1, text, { timeout: timeoutGap }),
2801
- promiseRetry(
2802
- async retry => {
2803
- const textPresent = await contextObject
2804
- .locator(`:has-text(${JSON.stringify(text)})`)
2805
- .first()
2806
- .isVisible()
2807
- if (!textPresent) retry(errorMessage)
2813
+ // Strategy 1: waitForFunction that checks both body AND title text
2814
+ // Use this.page instead of contextObject because FrameLocator doesn't have waitForFunction
2815
+ // Original only checked document.body.innerText, missing title text like "TestEd"
2816
+ this.page.waitForFunction(
2817
+ function (text) {
2818
+ // Check body text (original behavior)
2819
+ if (document.body && document.body.innerText && document.body.innerText.indexOf(text) > -1) {
2820
+ return true
2821
+ }
2822
+ // Check document title (fixes the TestEd in title issue)
2823
+ if (document.title && document.title.indexOf(text) > -1) {
2824
+ return true
2825
+ }
2826
+ return false
2808
2827
  },
2809
- { retries: 1000, minTimeout: 500, maxTimeout: 500, factor: 1 },
2828
+ text,
2829
+ { timeout: timeoutGap },
2810
2830
  ),
2811
- ])
2831
+ // Strategy 2: Native Playwright text locator (replaces problematic promiseRetry)
2832
+ contextObject
2833
+ .locator(`:has-text(${JSON.stringify(text)})`)
2834
+ .first()
2835
+ .waitFor({ timeout: waitTimeout }),
2836
+ ]).catch(err => {
2837
+ throw new Error(errorMessage)
2838
+ })
2812
2839
  }
2813
2840
 
2814
2841
  /**
@@ -3883,9 +3910,18 @@ function saveVideoForPage(page, name) {
3883
3910
  async function saveTraceForContext(context, name) {
3884
3911
  if (!context) return
3885
3912
  if (!context.tracing) return
3886
- const fileName = `${`${global.output_dir}${pathSeparator}trace${pathSeparator}${uuidv4()}_${clearString(name)}`.slice(0, 245)}.zip`
3887
- await context.tracing.stop({ path: fileName })
3888
- return fileName
3913
+ try {
3914
+ const fileName = `${`${global.output_dir}${pathSeparator}trace${pathSeparator}${uuidv4()}_${clearString(name)}`.slice(0, 245)}.zip`
3915
+ await context.tracing.stop({ path: fileName })
3916
+ return fileName
3917
+ } catch (err) {
3918
+ // Handle the case where tracing was not started or context is invalid
3919
+ if (err.message && err.message.includes('Must start tracing before stopping')) {
3920
+ // Tracing was never started on this context, silently skip
3921
+ return null
3922
+ }
3923
+ throw err
3924
+ }
3889
3925
  }
3890
3926
 
3891
3927
  async function highlightActiveElement(element) {