codeceptjs 4.0.0-rc.1 → 4.0.0-rc.10

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 (47) hide show
  1. package/README.md +39 -27
  2. package/bin/mcp-server.js +610 -0
  3. package/docs/webapi/appendField.mustache +5 -0
  4. package/docs/webapi/attachFile.mustache +12 -0
  5. package/docs/webapi/checkOption.mustache +1 -1
  6. package/docs/webapi/clearField.mustache +5 -0
  7. package/docs/webapi/dontSeeCurrentPathEquals.mustache +10 -0
  8. package/docs/webapi/dontSeeElement.mustache +4 -0
  9. package/docs/webapi/dontSeeInField.mustache +5 -0
  10. package/docs/webapi/fillField.mustache +5 -0
  11. package/docs/webapi/moveCursorTo.mustache +5 -1
  12. package/docs/webapi/seeCurrentPathEquals.mustache +10 -0
  13. package/docs/webapi/seeElement.mustache +4 -0
  14. package/docs/webapi/seeInField.mustache +5 -0
  15. package/docs/webapi/selectOption.mustache +5 -0
  16. package/docs/webapi/uncheckOption.mustache +1 -1
  17. package/lib/codecept.js +20 -17
  18. package/lib/command/init.js +0 -3
  19. package/lib/command/run-workers.js +1 -0
  20. package/lib/container.js +19 -4
  21. package/lib/element/WebElement.js +81 -2
  22. package/lib/els.js +12 -6
  23. package/lib/helper/Appium.js +8 -8
  24. package/lib/helper/Playwright.js +219 -137
  25. package/lib/helper/Puppeteer.js +207 -69
  26. package/lib/helper/WebDriver.js +179 -64
  27. package/lib/helper/errors/MultipleElementsFound.js +27 -110
  28. package/lib/helper/extras/elementSelection.js +58 -0
  29. package/lib/helper/scripts/dropFile.js +11 -0
  30. package/lib/html.js +14 -1
  31. package/lib/listener/globalRetry.js +32 -6
  32. package/lib/mocha/cli.js +10 -0
  33. package/lib/plugin/aiTrace.js +464 -0
  34. package/lib/plugin/retryFailedStep.js +28 -19
  35. package/lib/plugin/stepByStepReport.js +5 -1
  36. package/lib/step/config.js +15 -2
  37. package/lib/step/record.js +1 -1
  38. package/lib/utils.js +48 -0
  39. package/lib/workers.js +49 -7
  40. package/package.json +5 -3
  41. package/typings/index.d.ts +19 -0
  42. package/lib/listener/enhancedGlobalRetry.js +0 -110
  43. package/lib/plugin/enhancedRetryFailedStep.js +0 -99
  44. package/lib/plugin/htmlReporter.js +0 -3648
  45. package/lib/retryCoordinator.js +0 -207
  46. package/typings/promiseBasedTypes.d.ts +0 -9469
  47. package/typings/types.d.ts +0 -11402
@@ -1,11 +1,16 @@
1
1
  Checks that value of input field or textarea doesn't equal to given value
2
2
  Opposite to `seeInField`.
3
3
 
4
+ The third parameter is an optional context (CSS or XPath locator) to narrow the search.
5
+
4
6
  ```js
5
7
  I.dontSeeInField('email', 'user@user.com'); // field by name
6
8
  I.dontSeeInField({ css: 'form input.email' }, 'user@user.com'); // field by CSS
9
+ // within a context
10
+ I.dontSeeInField('Name', 'old_value', '.form-container');
7
11
  ```
8
12
 
9
13
  @param {CodeceptJS.LocatorOrString} field located by label|name|CSS|XPath|strict locator.
10
14
  @param {CodeceptJS.StringOrSecret} value value to check.
15
+ @param {?CodeceptJS.LocatorOrString} [context=null] (optional, `null` by default) element located by CSS | XPath | strict locator.
11
16
  @returns {void} automatically synchronized promise through #recorder
@@ -1,6 +1,8 @@
1
1
  Fills a text field or textarea, after clearing its value, with the given string.
2
2
  Field is located by name, label, CSS, or XPath.
3
3
 
4
+ The third parameter is an optional context (CSS or XPath locator) to narrow the search.
5
+
4
6
  ```js
5
7
  // by label
6
8
  I.fillField('Email', 'hello@world.com');
@@ -10,7 +12,10 @@ I.fillField('password', secret('123456'));
10
12
  I.fillField('form#login input[name=username]', 'John');
11
13
  // or by strict locator
12
14
  I.fillField({css: 'form#login input[name=username]'}, 'John');
15
+ // within a context
16
+ I.fillField('Name', 'John', '#section2');
13
17
  ```
14
18
  @param {CodeceptJS.LocatorOrString} field located by label|name|CSS|XPath|strict locator.
15
19
  @param {CodeceptJS.StringOrSecret} value text value to fill.
20
+ @param {?CodeceptJS.LocatorOrString} [context=null] (optional, `null` by default) element located by CSS | XPath | strict locator.
16
21
  @returns {void} automatically synchronized promise through #recorder
@@ -1,12 +1,16 @@
1
1
  Moves cursor to element matched by locator.
2
2
  Extra shift can be set with offsetX and offsetY options.
3
3
 
4
+ An optional `context` (as a second parameter) can be specified to narrow the search to an element within a parent.
5
+ When the second argument is a non-number (string or locator object), it is treated as context.
6
+
4
7
  ```js
5
8
  I.moveCursorTo('.tooltip');
6
9
  I.moveCursorTo('#submit', 5,5);
10
+ I.moveCursorTo('#submit', '.container');
7
11
  ```
8
12
 
9
13
  @param {CodeceptJS.LocatorOrString} locator located by CSS|XPath|strict locator.
10
- @param {number} [offsetX=0] (optional, `0` by default) X-axis offset.
14
+ @param {number|CodeceptJS.LocatorOrString} [offsetX=0] (optional, `0` by default) X-axis offset or context locator.
11
15
  @param {number} [offsetY=0] (optional, `0` by default) Y-axis offset.
12
16
  @returns {void} automatically synchronized promise through #recorder
@@ -0,0 +1,10 @@
1
+ Checks that current URL path matches the expected path.
2
+ Query strings and URL fragments are ignored.
3
+
4
+ ```js
5
+ I.seeCurrentPathEquals('/info'); // passes for '/info', '/info?user=1', '/info#section'
6
+ I.seeCurrentPathEquals('/'); // passes for '/', '/?user=ok', '/#top'
7
+ ```
8
+
9
+ @param {string} path value to check.
10
+ @returns {void} automatically synchronized promise through #recorder
@@ -1,8 +1,12 @@
1
1
  Checks that a given Element is visible
2
2
  Element is located by CSS or XPath.
3
3
 
4
+ The second parameter is a context (CSS or XPath locator) to narrow the search.
5
+
4
6
  ```js
5
7
  I.seeElement('#modal');
8
+ I.seeElement('#modal', '#container');
6
9
  ```
7
10
  @param {CodeceptJS.LocatorOrString} locator located by CSS|XPath|strict locator.
11
+ @param {?CodeceptJS.LocatorOrString} [context=null] (optional, `null` by default) element located by CSS | XPath | strict locator.
8
12
  @returns {void} automatically synchronized promise through #recorder
@@ -1,12 +1,17 @@
1
1
  Checks that the given input field or textarea equals to given value.
2
2
  For fuzzy locators, fields are matched by label text, the "name" attribute, CSS, and XPath.
3
3
 
4
+ The third parameter is an optional context (CSS or XPath locator) to narrow the search.
5
+
4
6
  ```js
5
7
  I.seeInField('Username', 'davert');
6
8
  I.seeInField({css: 'form textarea'},'Type your comment here');
7
9
  I.seeInField('form input[type=hidden]','hidden_value');
8
10
  I.seeInField('#searchform input','Search');
11
+ // within a context
12
+ I.seeInField('Name', 'John', '.form-container');
9
13
  ```
10
14
  @param {CodeceptJS.LocatorOrString} field located by label|name|CSS|XPath|strict locator.
11
15
  @param {CodeceptJS.StringOrSecret} value value to check.
16
+ @param {?CodeceptJS.LocatorOrString} [context=null] (optional, `null` by default) element located by CSS | XPath | strict locator.
12
17
  @returns {void} automatically synchronized promise through #recorder
@@ -2,6 +2,8 @@ Selects an option in a drop-down select.
2
2
  Field is searched by label | name | CSS | XPath.
3
3
  Option is selected by visible text or by value.
4
4
 
5
+ The third parameter is an optional context (CSS or XPath locator) to narrow the search.
6
+
5
7
  ```js
6
8
  I.selectOption('Choose Plan', 'Monthly'); // select by label
7
9
  I.selectOption('subscription', 'Monthly'); // match option by text
@@ -9,6 +11,8 @@ I.selectOption('subscription', '0'); // or by value
9
11
  I.selectOption('//form/select[@name=account]','Premium');
10
12
  I.selectOption('form select[name=account]', 'Premium');
11
13
  I.selectOption({css: 'form select[name=account]'}, 'Premium');
14
+ // within a context
15
+ I.selectOption('age', '21-60', '#section2');
12
16
  ```
13
17
 
14
18
  Provide an array for the second argument to select multiple options.
@@ -18,4 +22,5 @@ I.selectOption('Which OS do you use?', ['Android', 'iOS']);
18
22
  ```
19
23
  @param {LocatorOrString} select field located by label|name|CSS|XPath|strict locator.
20
24
  @param {string|Array<*>} option visible text or value of option.
25
+ @param {?CodeceptJS.LocatorOrString} [context=null] (optional, `null` by default) element located by CSS | XPath | strict locator.
21
26
  @returns {void} automatically synchronized promise through #recorder
@@ -1,7 +1,7 @@
1
1
  Unselects a checkbox or radio button.
2
2
  Element is located by label or name or CSS or XPath.
3
3
 
4
- The second parameter is a context (CSS or XPath locator) to narrow the search.
4
+ The second parameter is an optional context (CSS or XPath locator) to narrow the search.
5
5
 
6
6
  ```js
7
7
  I.uncheckOption('#agree');
package/lib/codecept.js CHANGED
@@ -120,23 +120,26 @@ class Codecept {
120
120
  * Executes hooks.
121
121
  */
122
122
  async runHooks() {
123
- // default hooks - dynamic imports for ESM
124
- const listenerModules = [
125
- './listener/store.js',
126
- './listener/steps.js',
127
- './listener/config.js',
128
- './listener/result.js',
129
- './listener/helpers.js',
130
- './listener/globalTimeout.js',
131
- './listener/globalRetry.js',
132
- './listener/retryEnhancer.js',
133
- './listener/exit.js',
134
- './listener/emptyRun.js',
135
- ]
136
-
137
- for (const modulePath of listenerModules) {
138
- const module = await import(modulePath)
139
- runHook(module.default || module)
123
+ // For workers parent process we only need plugins/hooks.
124
+ // Core listeners are executed inside worker threads.
125
+ if (!this.opts?.skipDefaultListeners) {
126
+ const listenerModules = [
127
+ './listener/store.js',
128
+ './listener/steps.js',
129
+ './listener/config.js',
130
+ './listener/result.js',
131
+ './listener/helpers.js',
132
+ './listener/globalTimeout.js',
133
+ './listener/globalRetry.js',
134
+ './listener/retryEnhancer.js',
135
+ './listener/exit.js',
136
+ './listener/emptyRun.js',
137
+ ]
138
+
139
+ for (const modulePath of listenerModules) {
140
+ const module = await import(modulePath)
141
+ runHook(module.default || module)
142
+ }
140
143
  }
141
144
 
142
145
  // custom hooks (previous iteration of plugins)
@@ -20,9 +20,6 @@ const defaultConfig = {
20
20
  helpers: {},
21
21
  include: {},
22
22
  plugins: {
23
- htmlReporter: {
24
- enabled: true,
25
- },
26
23
  },
27
24
  }
28
25
 
@@ -41,6 +41,7 @@ export default async function (workerCount, selectedRuns, options) {
41
41
  output.print(`CodeceptJS v${Codecept.version()} ${output.standWithUkraine()}`)
42
42
  output.print(`Running tests in ${output.styles.bold(numberOfWorkers)} workers...`)
43
43
  store.hasWorkers = true
44
+ process.env.RUNS_WITH_WORKERS = 'true'
44
45
 
45
46
  const workers = new Workers(numberOfWorkers, config)
46
47
  workers.overrideConfig(overrideConfigs)
package/lib/container.js CHANGED
@@ -657,13 +657,28 @@ async function createPlugins(config, options = {}) {
657
657
  const enabledPluginsByOptions = (options.plugins || '').split(',')
658
658
  for (const pluginName in config) {
659
659
  if (!config[pluginName]) config[pluginName] = {}
660
- if (!config[pluginName].enabled && enabledPluginsByOptions.indexOf(pluginName) < 0) {
660
+ const pluginConfig = config[pluginName]
661
+ if (!pluginConfig.enabled && enabledPluginsByOptions.indexOf(pluginName) < 0) {
661
662
  continue // plugin is disabled
662
663
  }
664
+
665
+ // Generic workers gate:
666
+ // - runInWorker / runInWorkers controls plugin execution inside worker threads.
667
+ // - runInParent / runInMain can disable plugin in workers parent process.
668
+ const runInWorker = pluginConfig.runInWorker ?? pluginConfig.runInWorkers ?? (pluginName === 'testomatio' ? false : true)
669
+ const runInParent = pluginConfig.runInParent ?? pluginConfig.runInMain ?? true
670
+
671
+ if (options.child && !runInWorker) {
672
+ continue
673
+ }
674
+
675
+ if (!options.child && process.env.RUNS_WITH_WORKERS === 'true' && !runInParent) {
676
+ continue
677
+ }
663
678
  let module
664
679
  try {
665
- if (config[pluginName].require) {
666
- module = config[pluginName].require
680
+ if (pluginConfig.require) {
681
+ module = pluginConfig.require
667
682
  if (module.startsWith('.')) {
668
683
  // local
669
684
  module = path.resolve(global.codecept_dir, module) // custom plugin
@@ -673,7 +688,7 @@ async function createPlugins(config, options = {}) {
673
688
  }
674
689
 
675
690
  // Use async loading for all plugins (ESM and CJS)
676
- plugins[pluginName] = await loadPluginAsync(module, config[pluginName])
691
+ plugins[pluginName] = await loadPluginAsync(module, pluginConfig)
677
692
  debug(`plugin ${pluginName} loaded via async import`)
678
693
  } catch (err) {
679
694
  throw new Error(`Could not load plugin ${pluginName} from module '${module}':\n${err.message}\n${err.stack}`)
@@ -1,4 +1,5 @@
1
1
  import assert from 'assert'
2
+ import { simplifyHtmlElement } from '../html.js'
2
3
 
3
4
  /**
4
5
  * Unified WebElement class that wraps native element instances from different helpers
@@ -81,6 +82,10 @@ class WebElement {
81
82
  async getProperty(name) {
82
83
  switch (this.helperType) {
83
84
  case 'playwright':
85
+ // For Locator objects, use inputValue() for the 'value' property
86
+ if (name === 'value' && this.element.inputValue) {
87
+ return this.element.inputValue()
88
+ }
84
89
  return this.element.evaluate((el, propName) => el[propName], name)
85
90
  case 'webdriver':
86
91
  return this.element.getProperty(name)
@@ -236,10 +241,15 @@ class WebElement {
236
241
  async type(text, options = {}) {
237
242
  switch (this.helperType) {
238
243
  case 'playwright':
244
+ // Playwright Locator objects use fill() instead of type()
245
+ if (this.element.fill) {
246
+ return this.element.fill(text, options)
247
+ }
239
248
  return this.element.type(text, options)
240
249
  case 'webdriver':
241
250
  return this.element.setValue(text)
242
251
  case 'puppeteer':
252
+ await this.element.evaluate(el => { el.value = '' })
243
253
  return this.element.type(text, options)
244
254
  default:
245
255
  throw new Error(`Unsupported helper type: ${this.helperType}`)
@@ -256,7 +266,18 @@ class WebElement {
256
266
 
257
267
  switch (this.helperType) {
258
268
  case 'playwright':
259
- childElement = await this.element.$(this._normalizeLocator(locator))
269
+ // Playwright Locator objects use locator() method
270
+ if (this.element.locator) {
271
+ const childLocator = this.element.locator(this._normalizeLocator(locator))
272
+ // Get the element handle from the locator
273
+ try {
274
+ childElement = await childLocator.elementHandle()
275
+ } catch (e) {
276
+ return null
277
+ }
278
+ } else {
279
+ childElement = await this.element.$(this._normalizeLocator(locator))
280
+ }
260
281
  break
261
282
  case 'webdriver':
262
283
  try {
@@ -285,7 +306,14 @@ class WebElement {
285
306
 
286
307
  switch (this.helperType) {
287
308
  case 'playwright':
288
- childElements = await this.element.$$(this._normalizeLocator(locator))
309
+ // Playwright Locator objects use locator() method
310
+ if (this.element.locator) {
311
+ const childLocator = this.element.locator(this._normalizeLocator(locator))
312
+ // Get all element handles from the locator
313
+ childElements = await childLocator.elementHandles()
314
+ } else {
315
+ childElements = await this.element.$$(this._normalizeLocator(locator))
316
+ }
289
317
  break
290
318
  case 'webdriver':
291
319
  childElements = await this.element.$$(this._normalizeLocator(locator))
@@ -306,6 +334,57 @@ class WebElement {
306
334
  * @returns {string} Normalized CSS selector
307
335
  * @private
308
336
  */
337
+ async toAbsoluteXPath() {
338
+ const xpathFn = (el) => {
339
+ const parts = []
340
+ let current = el
341
+ while (current && current.nodeType === Node.ELEMENT_NODE) {
342
+ let index = 0
343
+ let sibling = current.previousSibling
344
+ while (sibling) {
345
+ if (sibling.nodeType === Node.ELEMENT_NODE && sibling.tagName === current.tagName) {
346
+ index++
347
+ }
348
+ sibling = sibling.previousSibling
349
+ }
350
+ const tagName = current.tagName.toLowerCase()
351
+ const pathIndex = index > 0 ? `[${index + 1}]` : ''
352
+ parts.unshift(`${tagName}${pathIndex}`)
353
+ current = current.parentElement
354
+ }
355
+ return '//' + parts.join('/')
356
+ }
357
+
358
+ switch (this.helperType) {
359
+ case 'playwright':
360
+ return this.element.evaluate(xpathFn)
361
+ case 'puppeteer':
362
+ return this.element.evaluate(xpathFn)
363
+ case 'webdriver':
364
+ return this.helper.browser.execute(xpathFn, this.element)
365
+ default:
366
+ throw new Error(`Unsupported helper type: ${this.helperType}`)
367
+ }
368
+ }
369
+
370
+ async toOuterHTML() {
371
+ switch (this.helperType) {
372
+ case 'playwright':
373
+ return this.element.evaluate(el => el.outerHTML)
374
+ case 'puppeteer':
375
+ return this.element.evaluate(el => el.outerHTML)
376
+ case 'webdriver':
377
+ return this.helper.browser.execute(el => el.outerHTML, this.element)
378
+ default:
379
+ throw new Error(`Unsupported helper type: ${this.helperType}`)
380
+ }
381
+ }
382
+
383
+ async toSimplifiedHTML(maxLength = 300) {
384
+ const outerHTML = await this.toOuterHTML()
385
+ return simplifyHtmlElement(outerHTML, maxLength)
386
+ }
387
+
309
388
  _normalizeLocator(locator) {
310
389
  if (typeof locator === 'string') {
311
390
  return locator
package/lib/els.js CHANGED
@@ -6,10 +6,11 @@ import recordStep from './step/record.js'
6
6
  import FuncStep from './step/func.js'
7
7
  import { truth } from './assert/truth.js'
8
8
  import { isAsyncFunction, humanizeFunction } from './utils.js'
9
+ import WebElement from './element/WebElement.js'
9
10
 
10
11
  function element(purpose, locator, fn) {
11
12
  let stepConfig
12
- if (arguments[arguments.length - 1] instanceof StepConfig) {
13
+ if (StepConfig.isStepConfig(arguments[arguments.length - 1])) {
13
14
  stepConfig = arguments[arguments.length - 1]
14
15
  }
15
16
 
@@ -28,7 +29,8 @@ function element(purpose, locator, fn) {
28
29
  const els = await step.helper._locate(locator)
29
30
  output.debug(`Found ${els.length} elements, using first element`)
30
31
 
31
- return fn(els[0])
32
+ const wrapped = new WebElement(els[0], step.helper)
33
+ return fn(wrapped)
32
34
  },
33
35
  stepConfig,
34
36
  )
@@ -52,7 +54,8 @@ function eachElement(purpose, locator, fn) {
52
54
  let i = 0
53
55
  for (const el of els) {
54
56
  try {
55
- await fn(el, i)
57
+ const wrapped = new WebElement(el, step.helper)
58
+ await fn(wrapped, i)
56
59
  } catch (err) {
57
60
  output.error(`eachElement: failed operation on element #${i} ${el}`)
58
61
  errs.push(err)
@@ -74,7 +77,8 @@ function expectElement(locator, fn) {
74
77
  const els = await step.helper._locate(locator)
75
78
  output.debug(`Found ${els.length} elements, first will be used for assertion`)
76
79
 
77
- const result = await fn(els[0])
80
+ const wrapped = new WebElement(els[0], step.helper)
81
+ const result = await fn(wrapped)
78
82
  const assertion = truth(`element (${locator})`, fn.toString())
79
83
  assertion.assert(result)
80
84
  })
@@ -92,7 +96,8 @@ function expectAnyElement(locator, fn) {
92
96
 
93
97
  let found = false
94
98
  for (const el of els) {
95
- const result = await fn(el)
99
+ const wrapped = new WebElement(el, step.helper)
100
+ const result = await fn(wrapped)
96
101
  if (result) {
97
102
  found = true
98
103
  break
@@ -113,7 +118,8 @@ function expectAllElements(locator, fn) {
113
118
  let i = 1
114
119
  for (const el of els) {
115
120
  output.debug(`checking element #${i}: ${el}`)
116
- const result = await fn(el)
121
+ const wrapped = new WebElement(el, step.helper)
122
+ const result = await fn(wrapped)
117
123
  const assertion = truth(`element #${i} of (${locator})`, humanizeFunction(fn))
118
124
  assertion.assert(result)
119
125
  i++
@@ -1543,8 +1543,8 @@ class Appium extends Webdriver {
1543
1543
  /**
1544
1544
  * {{> dontSeeElement }}
1545
1545
  */
1546
- async dontSeeElement(locator) {
1547
- if (this.isWeb) return super.dontSeeElement(locator)
1546
+ async dontSeeElement(locator, context = null) {
1547
+ if (this.isWeb) return super.dontSeeElement(locator, context)
1548
1548
 
1549
1549
  // For mobile native apps, use safe isDisplayed wrapper
1550
1550
  const parsedLocator = parseLocator.call(this, locator)
@@ -1589,9 +1589,9 @@ class Appium extends Webdriver {
1589
1589
  * {{> fillField }}
1590
1590
  *
1591
1591
  */
1592
- async fillField(field, value) {
1592
+ async fillField(field, value, context = null) {
1593
1593
  value = value.toString()
1594
- if (this.isWeb) return super.fillField(field, value)
1594
+ if (this.isWeb) return super.fillField(field, value, context)
1595
1595
  return super.fillField(parseLocator.call(this, field), value)
1596
1596
  }
1597
1597
 
@@ -1706,8 +1706,8 @@ class Appium extends Webdriver {
1706
1706
  * {{> seeElement }}
1707
1707
  *
1708
1708
  */
1709
- async seeElement(locator) {
1710
- if (this.isWeb) return super.seeElement(locator)
1709
+ async seeElement(locator, context = null) {
1710
+ if (this.isWeb) return super.seeElement(locator, context)
1711
1711
 
1712
1712
  // For mobile native apps, use safe isDisplayed wrapper
1713
1713
  const parsedLocator = parseLocator.call(this, locator)
@@ -1754,8 +1754,8 @@ class Appium extends Webdriver {
1754
1754
  *
1755
1755
  * Supported only for web testing
1756
1756
  */
1757
- async selectOption(select, option) {
1758
- if (this.isWeb) return super.selectOption(select, option)
1757
+ async selectOption(select, option, context = null) {
1758
+ if (this.isWeb) return super.selectOption(select, option, context)
1759
1759
  throw new Error("Should be used only in Web context. In native context use 'click' method instead")
1760
1760
  }
1761
1761