codeceptjs 4.0.0-beta.6.esm-aria → 4.0.0-beta.8.esm-aria

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.
Files changed (69) hide show
  1. package/README.md +46 -3
  2. package/bin/codecept.js +9 -0
  3. package/bin/test-server.js +64 -0
  4. package/docs/webapi/click.mustache +5 -1
  5. package/lib/ai.js +66 -102
  6. package/lib/codecept.js +99 -24
  7. package/lib/command/generate.js +33 -1
  8. package/lib/command/init.js +7 -3
  9. package/lib/command/run-workers.js +31 -2
  10. package/lib/command/run.js +15 -0
  11. package/lib/command/workers/runTests.js +331 -58
  12. package/lib/config.js +16 -5
  13. package/lib/container.js +15 -13
  14. package/lib/effects.js +1 -1
  15. package/lib/element/WebElement.js +327 -0
  16. package/lib/event.js +10 -1
  17. package/lib/helper/AI.js +11 -11
  18. package/lib/helper/ApiDataFactory.js +34 -6
  19. package/lib/helper/Appium.js +156 -42
  20. package/lib/helper/GraphQL.js +3 -3
  21. package/lib/helper/GraphQLDataFactory.js +4 -4
  22. package/lib/helper/JSONResponse.js +48 -40
  23. package/lib/helper/Mochawesome.js +24 -2
  24. package/lib/helper/Playwright.js +841 -153
  25. package/lib/helper/Puppeteer.js +263 -67
  26. package/lib/helper/REST.js +21 -0
  27. package/lib/helper/WebDriver.js +116 -26
  28. package/lib/helper/clientscripts/PollyWebDriverExt.js +1 -1
  29. package/lib/helper/extras/PlaywrightReactVueLocator.js +52 -0
  30. package/lib/helper/extras/PlaywrightRestartOpts.js +12 -1
  31. package/lib/helper/network/actions.js +8 -6
  32. package/lib/listener/config.js +11 -3
  33. package/lib/listener/enhancedGlobalRetry.js +110 -0
  34. package/lib/listener/globalTimeout.js +19 -4
  35. package/lib/listener/helpers.js +8 -2
  36. package/lib/listener/retryEnhancer.js +85 -0
  37. package/lib/listener/steps.js +12 -0
  38. package/lib/mocha/asyncWrapper.js +13 -3
  39. package/lib/mocha/cli.js +1 -1
  40. package/lib/mocha/factory.js +3 -0
  41. package/lib/mocha/gherkin.js +1 -1
  42. package/lib/mocha/test.js +6 -0
  43. package/lib/mocha/ui.js +13 -0
  44. package/lib/output.js +62 -18
  45. package/lib/plugin/coverage.js +16 -3
  46. package/lib/plugin/enhancedRetryFailedStep.js +99 -0
  47. package/lib/plugin/htmlReporter.js +3648 -0
  48. package/lib/plugin/retryFailedStep.js +1 -0
  49. package/lib/plugin/stepByStepReport.js +1 -1
  50. package/lib/recorder.js +28 -3
  51. package/lib/result.js +100 -23
  52. package/lib/retryCoordinator.js +207 -0
  53. package/lib/step/base.js +1 -1
  54. package/lib/step/comment.js +2 -2
  55. package/lib/step/meta.js +1 -1
  56. package/lib/template/heal.js +1 -1
  57. package/lib/template/prompts/generatePageObject.js +31 -0
  58. package/lib/template/prompts/healStep.js +13 -0
  59. package/lib/template/prompts/writeStep.js +9 -0
  60. package/lib/test-server.js +334 -0
  61. package/lib/utils/mask_data.js +47 -0
  62. package/lib/utils.js +87 -6
  63. package/lib/workerStorage.js +2 -1
  64. package/lib/workers.js +179 -23
  65. package/package.json +60 -52
  66. package/translations/utils.js +2 -10
  67. package/typings/index.d.ts +19 -7
  68. package/typings/promiseBasedTypes.d.ts +5525 -3759
  69. package/typings/types.d.ts +5791 -3781
package/lib/container.js CHANGED
@@ -30,6 +30,7 @@ let container = {
30
30
  translation: {},
31
31
  /** @type {Result | null} */
32
32
  result: null,
33
+ sharedKeys: new Set() // Track keys shared via share() function
33
34
  }
34
35
 
35
36
  /**
@@ -220,6 +221,7 @@ class Container {
220
221
  container.translation = await loadTranslation()
221
222
  container.proxySupport = createSupportObjects(newSupport)
222
223
  container.plugins = newPlugins
224
+ container.sharedKeys = new Set() // Clear shared keys
223
225
  asyncHelperPromise = Promise.resolve()
224
226
  store.actor = null
225
227
  debug('container cleared')
@@ -243,7 +245,13 @@ class Container {
243
245
  * @param {Object} options - set {local: true} to not share among workers
244
246
  */
245
247
  static share(data, options = {}) {
246
- Container.append({ support: data })
248
+ // Instead of using append which replaces the entire container,
249
+ // directly update the support object to maintain proxy references
250
+ Object.assign(container.support, data)
251
+
252
+ // Track which keys were explicitly shared
253
+ Object.keys(data).forEach(key => container.sharedKeys.add(key))
254
+
247
255
  if (!options.local) {
248
256
  WorkerStorage.share(data)
249
257
  }
@@ -508,10 +516,11 @@ function createSupportObjects(config) {
508
516
  {},
509
517
  {
510
518
  has(target, key) {
511
- return keys.includes(key)
519
+ return keys.includes(key) || container.sharedKeys.has(key)
512
520
  },
513
521
  ownKeys() {
514
- return keys
522
+ // Return both original config keys and explicitly shared keys
523
+ return [...new Set([...keys, ...container.sharedKeys])]
515
524
  },
516
525
  getOwnPropertyDescriptor(target, prop) {
517
526
  return {
@@ -521,16 +530,9 @@ function createSupportObjects(config) {
521
530
  }
522
531
  },
523
532
  get(target, key) {
524
- if (typeof key === 'symbol') {
525
- // safely ignore symbol-based meta properties used by tooling
526
- return undefined
527
- }
528
- // Allow special I even if not declared in includes
529
- if (key === 'I') {
530
- return lazyLoad('I')
531
- }
532
- if (!keys.includes(key)) {
533
- throw new Error(`Support object "${String(key)}" is not defined`)
533
+ // First check if this is an explicitly shared property
534
+ if (container.sharedKeys.has(key) && key in container.support) {
535
+ return container.support[key]
534
536
  }
535
537
  return lazyLoad(key)
536
538
  },
package/lib/effects.js CHANGED
@@ -171,7 +171,7 @@ async function hopeThat(callback) {
171
171
  * - Restores the session state after each attempt, whether successful or not.
172
172
  *
173
173
  * @example
174
- * const { hopeThat } = require('codeceptjs/effects')
174
+ * const { retryTo } = require('codeceptjs/effects')
175
175
  * await retryTo((tries) => {
176
176
  * if (tries < 3) {
177
177
  * I.see('Non-existent element'); // Simulates a failure
@@ -0,0 +1,327 @@
1
+ import assert from '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
+ export default WebElement
package/lib/event.js CHANGED
@@ -3,13 +3,22 @@ const debug = debugModule('codeceptjs:event')
3
3
  import events from 'events'
4
4
  import output from './output.js'
5
5
 
6
+ const MAX_LISTENERS = 200
7
+
6
8
  const dispatcher = new events.EventEmitter()
7
9
 
8
- dispatcher.setMaxListeners(50)
10
+ dispatcher.setMaxListeners(MAX_LISTENERS)
11
+
12
+ // Increase process max listeners to prevent warnings for beforeExit and other events
13
+ if (typeof process.setMaxListeners === 'function') {
14
+ process.setMaxListeners(MAX_LISTENERS)
15
+ }
16
+
9
17
  /**
10
18
  * @namespace
11
19
  * @alias event
12
20
  */
21
+
13
22
  export default {
14
23
  /**
15
24
  * @type {NodeJS.EventEmitter}
package/lib/helper/AI.js CHANGED
@@ -1,13 +1,13 @@
1
- const HelperModule = require('@codeceptjs/helper')
2
- const ora = require('ora-classic')
3
- const fs = require('fs')
4
- const path = require('path')
5
- const ai = require('../ai')
6
- const Container = require('../container')
7
- const { splitByChunks, minifyHtml } = require('../html')
8
- const { beautify } = require('../utils')
9
- const output = require('../output')
10
- const { registerVariable } = require('../pause')
1
+ import HelperModule from '@codeceptjs/helper'
2
+ import ora from 'ora-classic'
3
+ import fs from 'fs'
4
+ import path from 'path'
5
+ import ai from '../ai.js'
6
+ import Container from '../container.js'
7
+ import { splitByChunks, minifyHtml } from '../html.js'
8
+ import { beautify } from '../utils.js'
9
+ import output from '../output.js'
10
+ import { registerVariable } from '../pause.js'
11
11
 
12
12
  const standardActingHelpers = Container.STANDARD_ACTING_HELPERS
13
13
 
@@ -211,4 +211,4 @@ class AI extends Helper {
211
211
  }
212
212
  }
213
213
 
214
- module.exports = AI
214
+ export default AI
@@ -241,10 +241,25 @@ class ApiDataFactory extends Helper {
241
241
  if (!createdItems.length) continue
242
242
  this.debug(`Deleting ${createdItems.length} ${factoryName}(s)`)
243
243
  for (const id in createdItems) {
244
- promises.push(this._requestDelete(factoryName, createdItems[id]))
244
+ const deletePromise = this._requestDelete(factoryName, createdItems[id])
245
+ if (deletePromise) {
246
+ promises.push(deletePromise.catch(err => {
247
+ this.debugSection('Delete Error', `Failed to delete ${factoryName} with id ${createdItems[id]}: ${err.message}`)
248
+ // Don't reject Promise.all, just log the error
249
+ return Promise.resolve()
250
+ }))
251
+ }
245
252
  }
246
253
  }
247
- return Promise.all(promises)
254
+ return Promise.all(promises).then(() => {
255
+ // Clear the created items after successful cleanup
256
+ for (const factoryName in this.created) {
257
+ this.created[factoryName] = []
258
+ }
259
+ // Add a small delay to ensure file system changes propagate in Docker environments
260
+ // This helps avoid race conditions where GET requests after DELETE don't see the changes yet
261
+ return new Promise(resolve => setTimeout(resolve, 100))
262
+ })
248
263
  }
249
264
 
250
265
  /**
@@ -386,21 +401,34 @@ Current file error: ${err.message}`)
386
401
  const url = this.factories[factory].delete[method].replace('{id}', id)
387
402
 
388
403
  request = {
389
- method,
404
+ method: method.toUpperCase(), // Ensure HTTP method is uppercase
390
405
  url,
391
406
  }
392
407
  }
393
408
 
409
+ // Ensure method is uppercase (some servers require uppercase HTTP methods)
410
+ if (request.method) {
411
+ request.method = request.method.toUpperCase()
412
+ }
413
+
394
414
  request.baseURL = this.config.endpoint
395
415
 
396
416
  if (request.url.match(/^undefined/)) {
397
417
  return this.debugSection('Please configure the delete request in your ApiDataFactory helper', "delete: () => ({ method: 'DELETE', url: '/api/users' })")
398
418
  }
399
419
 
400
- return this.restHelper._executeRequest(request).then(() => {
420
+ this.debugSection('Deleting', `${request.method} ${request.baseURL}${request.url} (ID: ${id})`)
421
+
422
+ return this.restHelper._executeRequest(request).then((resp) => {
401
423
  const idx = this.created[factory].indexOf(id)
402
- this.debugSection('Deleted Id', `Id: ${id}`)
403
- this.created[factory].splice(idx, 1)
424
+ this.debugSection('Deleted Successfully', `${factory} ID: ${id}, Status: ${resp.status || 'OK'}`)
425
+ if (idx !== -1) {
426
+ this.created[factory].splice(idx, 1)
427
+ }
428
+ return resp
429
+ }).catch(err => {
430
+ this.debugSection('Delete Failed', `${factory} ID: ${id}, Error: ${err.message}`)
431
+ throw err
404
432
  })
405
433
  }
406
434
  }