codeceptjs 3.7.4 → 3.7.5-beta.2

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.
@@ -20,7 +20,7 @@ const stderr = ''
20
20
  // Requiring of Codecept need to be after tty.getWindowSize is available.
21
21
  const Codecept = require(process.env.CODECEPT_CLASS_PATH || '../../codecept')
22
22
 
23
- const { options, tests, testRoot, workerIndex } = workerData
23
+ const { options, tests, testRoot, workerIndex, poolMode } = workerData
24
24
 
25
25
  // hide worker output
26
26
  if (!options.debug && !options.verbose)
@@ -39,15 +39,26 @@ const codecept = new Codecept(config, options)
39
39
  codecept.init(testRoot)
40
40
  codecept.loadTests()
41
41
  const mocha = container.mocha()
42
- filterTests()
42
+
43
+ if (poolMode) {
44
+ // In pool mode, don't filter tests upfront - wait for assignments
45
+ // We'll reload test files fresh for each test request
46
+ } else {
47
+ // Legacy mode - filter tests upfront
48
+ filterTests()
49
+ }
43
50
 
44
51
  // run tests
45
52
  ;(async function () {
46
- if (mocha.suite.total()) {
53
+ if (poolMode) {
54
+ await runPoolTests()
55
+ } else if (mocha.suite.total()) {
47
56
  await runTests()
48
57
  }
49
58
  })()
50
59
 
60
+ let globalStats = { passes: 0, failures: 0, tests: 0, pending: 0, failedHooks: 0 }
61
+
51
62
  async function runTests() {
52
63
  try {
53
64
  await codecept.bootstrap()
@@ -64,6 +75,192 @@ async function runTests() {
64
75
  }
65
76
  }
66
77
 
78
+ async function runPoolTests() {
79
+ try {
80
+ await codecept.bootstrap()
81
+ } catch (err) {
82
+ throw new Error(`Error while running bootstrap file :${err}`)
83
+ }
84
+
85
+ initializeListeners()
86
+ disablePause()
87
+
88
+ // Accumulate results across all tests in pool mode
89
+ let consolidatedStats = { passes: 0, failures: 0, tests: 0, pending: 0, failedHooks: 0 }
90
+ let allTests = []
91
+ let allFailures = []
92
+ let previousStats = { passes: 0, failures: 0, tests: 0, pending: 0, failedHooks: 0 }
93
+
94
+ // Keep requesting tests until no more available
95
+ while (true) {
96
+ // Request a test assignment
97
+ sendToParentThread({ type: 'REQUEST_TEST', workerIndex })
98
+
99
+ const testResult = await new Promise((resolve, reject) => {
100
+ // Set up pool mode message handler
101
+ const messageHandler = async eventData => {
102
+ if (eventData.type === 'TEST_ASSIGNED') {
103
+ const testUid = eventData.test
104
+
105
+ try {
106
+ // In pool mode, we need to create a fresh Mocha instance for each test
107
+ // because Mocha instances become disposed after running tests
108
+ container.createMocha() // Create fresh Mocha instance
109
+ filterTestById(testUid)
110
+ const mocha = container.mocha()
111
+
112
+ if (mocha.suite.total() > 0) {
113
+ // Run the test and complete
114
+ await codecept.run()
115
+
116
+ // Get the results from this specific test run
117
+ const result = container.result()
118
+ const currentStats = result.stats || {}
119
+
120
+ // Calculate the difference from previous accumulated stats
121
+ const newPasses = Math.max(0, (currentStats.passes || 0) - previousStats.passes)
122
+ const newFailures = Math.max(0, (currentStats.failures || 0) - previousStats.failures)
123
+ const newTests = Math.max(0, (currentStats.tests || 0) - previousStats.tests)
124
+ const newPending = Math.max(0, (currentStats.pending || 0) - previousStats.pending)
125
+ const newFailedHooks = Math.max(0, (currentStats.failedHooks || 0) - previousStats.failedHooks)
126
+
127
+ // Add only the new results
128
+ consolidatedStats.passes += newPasses
129
+ consolidatedStats.failures += newFailures
130
+ consolidatedStats.tests += newTests
131
+ consolidatedStats.pending += newPending
132
+ consolidatedStats.failedHooks += newFailedHooks
133
+
134
+ // Update previous stats for next comparison
135
+ previousStats = { ...currentStats }
136
+
137
+ // Add new failures to consolidated collections
138
+ if (result.failures && result.failures.length > allFailures.length) {
139
+ const newFailures = result.failures.slice(allFailures.length)
140
+ allFailures.push(...newFailures)
141
+ }
142
+ }
143
+
144
+ // Signal test completed and request next
145
+ parentPort?.off('message', messageHandler)
146
+ resolve('TEST_COMPLETED')
147
+ } catch (err) {
148
+ parentPort?.off('message', messageHandler)
149
+ reject(err)
150
+ }
151
+ } else if (eventData.type === 'NO_MORE_TESTS') {
152
+ // No tests available, exit worker
153
+ parentPort?.off('message', messageHandler)
154
+ resolve('NO_MORE_TESTS')
155
+ } else {
156
+ // Handle other message types (support messages, etc.)
157
+ container.append({ support: eventData.data })
158
+ }
159
+ }
160
+
161
+ parentPort?.on('message', messageHandler)
162
+ })
163
+
164
+ // Exit if no more tests
165
+ if (testResult === 'NO_MORE_TESTS') {
166
+ break
167
+ }
168
+ }
169
+
170
+ try {
171
+ await codecept.teardown()
172
+ } catch (err) {
173
+ // Log teardown errors but don't fail
174
+ console.error('Teardown error:', err)
175
+ }
176
+
177
+ // Send final consolidated results for the entire worker
178
+ const finalResult = {
179
+ hasFailed: consolidatedStats.failures > 0,
180
+ stats: consolidatedStats,
181
+ duration: 0, // Pool mode doesn't track duration per worker
182
+ tests: [], // Keep tests empty to avoid serialization issues - stats are sufficient
183
+ failures: allFailures, // Include all failures for error reporting
184
+ }
185
+
186
+ sendToParentThread({ event: event.all.after, workerIndex, data: finalResult })
187
+ sendToParentThread({ event: event.all.result, workerIndex, data: finalResult })
188
+
189
+ // Add longer delay to ensure messages are delivered before closing
190
+ await new Promise(resolve => setTimeout(resolve, 100))
191
+
192
+ // Close worker thread when pool mode is complete
193
+ parentPort?.close()
194
+ }
195
+
196
+ function filterTestById(testUid) {
197
+ // Reload test files fresh for each test in pool mode
198
+ const files = codecept.testFiles
199
+
200
+ // Get the existing mocha instance
201
+ const mocha = container.mocha()
202
+
203
+ // Clear suites and tests but preserve other mocha settings
204
+ mocha.suite.suites = []
205
+ mocha.suite.tests = []
206
+
207
+ // Clear require cache for test files to ensure fresh loading
208
+ files.forEach(file => {
209
+ delete require.cache[require.resolve(file)]
210
+ })
211
+
212
+ // Set files and load them
213
+ mocha.files = files
214
+ mocha.loadFiles()
215
+
216
+ // Now filter to only the target test - use a more robust approach
217
+ let foundTest = false
218
+ for (const suite of mocha.suite.suites) {
219
+ const originalTests = [...suite.tests]
220
+ suite.tests = []
221
+
222
+ for (const test of originalTests) {
223
+ if (test.uid === testUid) {
224
+ suite.tests.push(test)
225
+ foundTest = true
226
+ break // Only add one matching test
227
+ }
228
+ }
229
+
230
+ // If no tests found in this suite, remove it
231
+ if (suite.tests.length === 0) {
232
+ suite.parent.suites = suite.parent.suites.filter(s => s !== suite)
233
+ }
234
+ }
235
+
236
+ // Filter out empty suites from the root
237
+ mocha.suite.suites = mocha.suite.suites.filter(suite => suite.tests.length > 0)
238
+
239
+ if (!foundTest) {
240
+ // If testUid doesn't match, maybe it's a simple test name - try fallback
241
+ mocha.suite.suites = []
242
+ mocha.suite.tests = []
243
+ mocha.loadFiles()
244
+
245
+ // Try matching by title
246
+ for (const suite of mocha.suite.suites) {
247
+ const originalTests = [...suite.tests]
248
+ suite.tests = []
249
+
250
+ for (const test of originalTests) {
251
+ if (test.title === testUid || test.fullTitle() === testUid || test.uid === testUid) {
252
+ suite.tests.push(test)
253
+ foundTest = true
254
+ break
255
+ }
256
+ }
257
+ }
258
+
259
+ // Clean up empty suites again
260
+ mocha.suite.suites = mocha.suite.suites.filter(suite => suite.tests.length > 0)
261
+ }
262
+ }
263
+
67
264
  function filterTests() {
68
265
  const files = codecept.testFiles
69
266
  mocha.files = files
@@ -102,14 +299,20 @@ function initializeListeners() {
102
299
  event.dispatcher.on(event.hook.passed, hook => sendToParentThread({ event: event.hook.passed, workerIndex, data: hook.simplify() }))
103
300
  event.dispatcher.on(event.hook.finished, hook => sendToParentThread({ event: event.hook.finished, workerIndex, data: hook.simplify() }))
104
301
 
105
- event.dispatcher.once(event.all.after, () => {
106
- sendToParentThread({ event: event.all.after, workerIndex, data: container.result().simplify() })
107
- })
108
- // all
109
- event.dispatcher.once(event.all.result, () => {
110
- sendToParentThread({ event: event.all.result, workerIndex, data: container.result().simplify() })
111
- parentPort?.close()
112
- })
302
+ if (!poolMode) {
303
+ // In regular mode, close worker after all tests are complete
304
+ event.dispatcher.once(event.all.after, () => {
305
+ sendToParentThread({ event: event.all.after, workerIndex, data: container.result().simplify() })
306
+ })
307
+ // all
308
+ event.dispatcher.once(event.all.result, () => {
309
+ sendToParentThread({ event: event.all.result, workerIndex, data: container.result().simplify() })
310
+ parentPort?.close()
311
+ })
312
+ } else {
313
+ // In pool mode, don't send result events for individual tests
314
+ // Results will be sent once when the worker completes all tests
315
+ }
113
316
  }
114
317
 
115
318
  function disablePause() {
@@ -121,7 +324,10 @@ function sendToParentThread(data) {
121
324
  }
122
325
 
123
326
  function listenToParentThread() {
124
- parentPort?.on('message', eventData => {
125
- container.append({ support: eventData.data })
126
- })
327
+ if (!poolMode) {
328
+ parentPort?.on('message', eventData => {
329
+ container.append({ support: eventData.data })
330
+ })
331
+ }
332
+ // In pool mode, message handling is done in runPoolTests()
127
333
  }
@@ -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
  }
@@ -37,7 +37,20 @@ 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) {
@@ -64,7 +77,16 @@ class Mochawesome extends Helper {
64
77
 
65
78
  addMochawesomeContext(context) {
66
79
  if (currentTest === '') currentTest = { test: currentSuite.ctx.test }
67
- return this._addContext(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)
68
90
  }
69
91
  }
70
92