codeceptjs 3.7.0-rc.1 → 3.7.1-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.
package/bin/codecept.js CHANGED
@@ -62,7 +62,7 @@ program
62
62
  .command('check')
63
63
  .option(commandFlags.config.flag, commandFlags.config.description)
64
64
  .description('Checks configuration and environment before running tests')
65
- .option('-t, --timeout [ms]', 'timeout for checks in ms, 20000 by default')
65
+ .option('-t, --timeout [ms]', 'timeout for checks in ms, 50000 by default')
66
66
  .action(errorHandler(require('../lib/command/check')))
67
67
 
68
68
  program
@@ -25,6 +25,7 @@ module.exports = async function (options) {
25
25
  ai: true, // we don't need to check AI
26
26
  helpers: false,
27
27
  setup: false,
28
+ teardown: false,
28
29
  tests: false,
29
30
  def: false,
30
31
  }
@@ -87,9 +88,9 @@ module.exports = async function (options) {
87
88
 
88
89
  if (config?.ai?.request) {
89
90
  checks.ai = true
90
- printCheck('ai', checks['ai'], 'AI configuration is enabled, request function is set')
91
+ printCheck('ai', checks['ai'], 'Configuration is enabled, request function is set')
91
92
  } else {
92
- printCheck('ai', checks['ai'], 'AI is disabled')
93
+ printCheck('ai', checks['ai'], 'Disabled')
93
94
  }
94
95
 
95
96
  printCheck('tests', checks['tests'], `Total: ${numTests} tests`)
@@ -131,22 +132,36 @@ module.exports = async function (options) {
131
132
  if (Object.keys(helpers).length) {
132
133
  const suite = container.mocha().suite
133
134
  const test = createTest('test', () => {})
134
- try {
135
- for (const helper of Object.values(helpers)) {
135
+ checks.setup = true
136
+ for (const helper of Object.values(helpers)) {
137
+ try {
136
138
  if (helper._beforeSuite) await helper._beforeSuite(suite)
137
139
  if (helper._before) await helper._before(test)
140
+ } catch (err) {
141
+ err.message = `${helper.constructor.name} helper: ${err.message}`
142
+ if (checks.setup instanceof Error) err.message = `${err.message}\n\n${checks.setup?.message || ''}`.trim()
143
+ checks.setup = err
144
+ }
145
+ }
146
+
147
+ printCheck('Helpers Before', checks['setup'], standardActingHelpers.some(h => Object.keys(helpers).includes(h)) ? 'Initializing browser' : '')
148
+
149
+ checks.teardown = true
150
+ for (const helper of Object.values(helpers).reverse()) {
151
+ try {
138
152
  if (helper._passed) await helper._passed(test)
139
153
  if (helper._after) await helper._after(test)
140
154
  if (helper._finishTest) await helper._finishTest(suite)
141
155
  if (helper._afterSuite) await helper._afterSuite(suite)
156
+ } catch (err) {
157
+ err.message = `${helper.constructor.name} helper: ${err.message}`
158
+ if (checks.teardown instanceof Error) err.message = `${err.message}\n\n${checks.teardown?.message || ''}`.trim()
159
+ checks.teardown = err
142
160
  }
143
- checks.setup = true
144
- } catch (err) {
145
- checks.setup = err
146
161
  }
147
- }
148
162
 
149
- printCheck('Helpers Before/After', checks['setup'], standardActingHelpers.some(h => Object.keys(helpers).includes(h)) ? 'Initializing and closing browser' : '')
163
+ printCheck('Helpers After', checks['teardown'], standardActingHelpers.some(h => Object.keys(helpers).includes(h)) ? 'Closing browser' : '')
164
+ }
150
165
 
151
166
  try {
152
167
  definitions(configFile, { dryRun: true })
package/lib/effects.js CHANGED
@@ -2,6 +2,7 @@ const recorder = require('./recorder')
2
2
  const { debug } = require('./output')
3
3
  const store = require('./store')
4
4
  const event = require('./event')
5
+ const within = require('./within')
5
6
 
6
7
  /**
7
8
  * A utility function for CodeceptJS tests that acts as a soft assertion.
@@ -178,11 +179,14 @@ async function tryTo(callback) {
178
179
  const sessionName = 'tryTo'
179
180
 
180
181
  let result = false
182
+ let isAutoRetriesEnabled = store.autoRetries
181
183
  return recorder.add(
182
184
  sessionName,
183
185
  () => {
184
186
  recorder.session.start(sessionName)
185
- store.tryTo = true
187
+ isAutoRetriesEnabled = store.autoRetries
188
+ if (isAutoRetriesEnabled) debug('Auto retries disabled inside tryTo effect')
189
+ store.autoRetries = false
186
190
  callback()
187
191
  recorder.add(() => {
188
192
  result = true
@@ -199,7 +203,7 @@ async function tryTo(callback) {
199
203
  return recorder.add(
200
204
  'result',
201
205
  () => {
202
- store.tryTo = undefined
206
+ store.autoRetries = isAutoRetriesEnabled
203
207
  return result
204
208
  },
205
209
  true,
@@ -215,4 +219,5 @@ module.exports = {
215
219
  hopeThat,
216
220
  retryTo,
217
221
  tryTo,
222
+ within,
218
223
  }
package/lib/event.js CHANGED
@@ -54,6 +54,8 @@ module.exports = {
54
54
  * @inner
55
55
  * @property {'hook.start'} started
56
56
  * @property {'hook.passed'} passed
57
+ * @property {'hook.failed'} failed
58
+ * @property {'hook.finished'} finished
57
59
  */
58
60
  hook: {
59
61
  started: 'hook.start',
@@ -484,7 +484,7 @@ class Playwright extends Helper {
484
484
  this.currentRunningTest = test
485
485
 
486
486
  recorder.retry({
487
- retries: process.env.FAILED_STEP_RETRIES || 3,
487
+ retries: test?.opts?.conditionalRetries || 3,
488
488
  when: err => {
489
489
  if (!err || typeof err.message !== 'string') {
490
490
  return false
@@ -312,7 +312,7 @@ class Puppeteer extends Helper {
312
312
  this.sessionPages = {}
313
313
  this.currentRunningTest = test
314
314
  recorder.retry({
315
- retries: process.env.FAILED_STEP_RETRIES || 3,
315
+ retries: test?.opts?.conditionalRetries || 3,
316
316
  when: err => {
317
317
  if (!err || typeof err.message !== 'string') {
318
318
  return false
@@ -2,66 +2,66 @@
2
2
  * Class to handle the interaction with the Dialog (Popup) Class from Puppeteer
3
3
  */
4
4
  class Popup {
5
- constructor(popup, defaultAction) {
6
- this._popup = popup || null;
7
- this._actionType = '';
8
- this._defaultAction = defaultAction || '';
5
+ constructor(popup = null, defaultAction = '') {
6
+ this._popup = popup
7
+ this._actionType = ''
8
+ this._defaultAction = defaultAction
9
9
  }
10
10
 
11
11
  _assertValidActionType(action) {
12
12
  if (['accept', 'cancel'].indexOf(action) === -1) {
13
- throw new Error('Invalid Popup action type. Only "accept" or "cancel" actions are accepted');
13
+ throw new Error('Invalid Popup action type. Only "accept" or "cancel" actions are accepted')
14
14
  }
15
15
  }
16
16
 
17
17
  set defaultAction(action) {
18
- this._assertValidActionType(action);
19
- this._defaultAction = action;
18
+ this._assertValidActionType(action)
19
+ this._defaultAction = action
20
20
  }
21
21
 
22
22
  get defaultAction() {
23
- return this._defaultAction;
23
+ return this._defaultAction
24
24
  }
25
25
 
26
26
  get popup() {
27
- return this._popup;
27
+ return this._popup
28
28
  }
29
29
 
30
30
  set popup(popup) {
31
31
  if (this._popup) {
32
- console.error('Popup already exists and was not closed. Popups must always be closed by calling either I.acceptPopup() or I.cancelPopup()');
32
+ return
33
33
  }
34
- this._popup = popup;
34
+ this._popup = popup
35
35
  }
36
36
 
37
37
  get actionType() {
38
- return this._actionType;
38
+ return this._actionType
39
39
  }
40
40
 
41
41
  set actionType(action) {
42
- this._assertValidActionType(action);
43
- this._actionType = action;
42
+ this._assertValidActionType(action)
43
+ this._actionType = action
44
44
  }
45
45
 
46
46
  clear() {
47
- this._popup = null;
48
- this._actionType = '';
47
+ this._popup = null
48
+ this._actionType = ''
49
49
  }
50
50
 
51
51
  assertPopupVisible() {
52
52
  if (!this._popup) {
53
- throw new Error('There is no Popup visible');
53
+ throw new Error('There is no Popup visible')
54
54
  }
55
55
  }
56
56
 
57
57
  assertPopupActionType(type) {
58
- this.assertPopupVisible();
59
- const expectedAction = this._actionType || this._defaultAction;
58
+ this.assertPopupVisible()
59
+ const expectedAction = this._actionType || this._defaultAction
60
60
  if (expectedAction !== type) {
61
- throw new Error(`Popup action does not fit the expected action type. Expected popup action to be '${expectedAction}' not '${type}`);
61
+ throw new Error(`Popup action does not fit the expected action type. Expected popup action to be '${expectedAction}' not '${type}`)
62
62
  }
63
- this.clear();
63
+ this.clear()
64
64
  }
65
65
  }
66
66
 
67
- module.exports = Popup;
67
+ module.exports = Popup
package/lib/mocha/test.js CHANGED
@@ -133,8 +133,10 @@ function cloneTest(test) {
133
133
  return deserializeTest(serializeTest(test))
134
134
  }
135
135
 
136
- function testToFileName(test) {
137
- let fileName = clearString(test.title)
136
+ function testToFileName(test, suffix = '') {
137
+ let fileName = test.title
138
+
139
+ if (suffix) fileName = `${fileName}_${suffix}`
138
140
  // remove tags with empty string (disable for now)
139
141
  // fileName = fileName.replace(/\@\w+/g, '')
140
142
  fileName = fileName.slice(0, 100)
@@ -146,6 +148,7 @@ function testToFileName(test) {
146
148
  // if (test.parent && test.parent.title) {
147
149
  // fileName = `${clearString(test.parent.title)}_${fileName}`
148
150
  // }
151
+ fileName = clearString(fileName).slice(0, 100)
149
152
  return fileName
150
153
  }
151
154
 
@@ -60,7 +60,7 @@ const defaultConfig = {
60
60
 
61
61
  If there is no groups of tests, say: "No patterns found"
62
62
  Preserve error messages but cut them if they are too long.
63
- Respond clearly and directly, without introductory words or phrases like Of course,’ Here is the answer,’ etc.
63
+ Respond clearly and directly, without introductory words or phrases like 'Of course,' 'Here is the answer,' etc.
64
64
  Do not list more than 3 errors in the group.
65
65
  If you identify that all tests in the group have the same tag, add this tag to the group report, otherwise ignore TAG section.
66
66
  If you identify that all tests in the group have the same suite, add this suite to the group report, otherwise ignore SUITE section.
@@ -161,8 +161,56 @@ const defaultConfig = {
161
161
 
162
162
  /**
163
163
  *
164
- * @param {*} config
165
- * @returns
164
+ * Uses AI to analyze test failures and provide insights
165
+ *
166
+ * This plugin analyzes failed tests using AI to provide detailed explanations and group similar failures.
167
+ * When enabled with --ai flag, it generates reports after test execution.
168
+ *
169
+ * #### Usage
170
+ *
171
+ * ```js
172
+ * // in codecept.conf.js
173
+ * exports.config = {
174
+ * plugins: {
175
+ * analyze: {
176
+ * enabled: true,
177
+ * clusterize: 5,
178
+ * analyze: 2,
179
+ * vision: false
180
+ * }
181
+ * }
182
+ * }
183
+ * ```
184
+ *
185
+ * #### Configuration
186
+ *
187
+ * * `clusterize` (number) - minimum number of failures to trigger clustering analysis. Default: 5
188
+ * * `analyze` (number) - maximum number of individual test failures to analyze in detail. Default: 2
189
+ * * `vision` (boolean) - enables visual analysis of test screenshots. Default: false
190
+ * * `categories` (array) - list of failure categories for classification. Defaults to:
191
+ * - Browser connection error / browser crash
192
+ * - Network errors (server error, timeout, etc)
193
+ * - HTML / page elements (not found, not visible, etc)
194
+ * - Navigation errors (404, etc)
195
+ * - Code errors (syntax error, JS errors, etc)
196
+ * - Library & framework errors
197
+ * - Data errors (password incorrect, invalid format, etc)
198
+ * - Assertion failures
199
+ * - Other errors
200
+ * * `prompts` (object) - customize AI prompts for analysis
201
+ * - `clusterize` - prompt for clustering analysis
202
+ * - `analyze` - prompt for individual test analysis
203
+ *
204
+ * #### Features
205
+ *
206
+ * * Groups similar failures when number of failures >= clusterize value
207
+ * * Provides detailed analysis of individual failures
208
+ * * Analyzes screenshots if vision=true and screenshots are available
209
+ * * Classifies failures into predefined categories
210
+ * * Suggests possible causes and solutions
211
+ *
212
+ * @param {Object} config - Plugin configuration
213
+ * @returns {void}
166
214
  */
167
215
  module.exports = function (config = {}) {
168
216
  config = Object.assign(defaultConfig, config)
@@ -1,8 +1,6 @@
1
1
  const event = require('../event')
2
2
  const recorder = require('../recorder')
3
- const container = require('../container')
4
- const { log } = require('../output')
5
-
3
+ const store = require('../store')
6
4
  const defaultConfig = {
7
5
  retries: 3,
8
6
  defaultIgnoredSteps: ['amOnPage', 'wait*', 'send*', 'execute*', 'run*', 'have*'],
@@ -70,9 +68,9 @@ const defaultConfig = {
70
68
  * Use scenario configuration to disable plugin for a test
71
69
  *
72
70
  * ```js
73
- * Scenario('scenario tite', () => {
71
+ * Scenario('scenario tite', { disableRetryFailedStep: true }, () => {
74
72
  * // test goes here
75
- * }).config(test => test.disableRetryFailedStep = true)
73
+ * })
76
74
  * ```
77
75
  *
78
76
  */
@@ -85,19 +83,14 @@ module.exports = config => {
85
83
 
86
84
  const when = err => {
87
85
  if (!enableRetry) return
88
- const store = require('../store')
89
86
  if (store.debugMode) return false
87
+ if (!store.autoRetries) return false
90
88
  if (customWhen) return customWhen(err)
91
89
  return true
92
90
  }
93
91
  config.when = when
94
92
 
95
93
  event.dispatcher.on(event.step.started, step => {
96
- if (process.env.TRY_TO === 'true') {
97
- log('Info: RetryFailedStep plugin is disabled inside tryTo block')
98
- return
99
- }
100
-
101
94
  // if a step is ignored - return
102
95
  for (const ignored of config.ignoredSteps) {
103
96
  if (step.name === ignored) return
@@ -113,9 +106,15 @@ module.exports = config => {
113
106
  })
114
107
 
115
108
  event.dispatcher.on(event.test.before, test => {
116
- if (test && test.disableRetryFailedStep) return // disable retry when a test is not active
117
- // this env var is used to set the retries inside _before() block of helpers
118
- process.env.FAILED_STEP_RETRIES = config.retries
109
+ // pass disableRetryFailedStep is a preferred way to disable retries
110
+ // test.disableRetryFailedStep is used for backward compatibility
111
+ if (test.opts.disableRetryFailedStep || test.disableRetryFailedStep) {
112
+ store.autoRetries = false
113
+ return // disable retry when a test is not active
114
+ }
115
+ // this option is used to set the retries inside _before() block of helpers
116
+ store.autoRetries = true
117
+ test.opts.conditionalRetries = config.retries
119
118
  recorder.retry(config)
120
119
  })
121
120
  }
@@ -6,7 +6,7 @@ const defaultConfig = {
6
6
 
7
7
  module.exports = function (config) {
8
8
  config = Object.assign(defaultConfig, config)
9
- console.log(`Deprecation Warning: 'retryTo' has been moved to the 'codeceptjs/effects' module`)
9
+ console.log(`Deprecation Warning: 'retryTo' has been moved to the 'codeceptjs/effects' module. Disable retryTo plugin to remove this warning.`)
10
10
 
11
11
  if (config.registerGlobal) {
12
12
  global.retryTo = retryTo
@@ -83,13 +83,12 @@ module.exports = function (config) {
83
83
  async () => {
84
84
  const dataType = 'image/png'
85
85
  // This prevents data driven to be included in the failed screenshot file name
86
- let fileName = testToFileName(test)
86
+ let fileName
87
87
 
88
88
  if (options.uniqueScreenshotNames && test) {
89
- const uuid = _getUUID(test)
90
- fileName = `${fileName.substring(0, 10)}_${uuid}.failed.png`
89
+ fileName = `${testToFileName(test, _getUUID(test))}.failed.png`
91
90
  } else {
92
- fileName += '.failed.png'
91
+ fileName = `${testToFileName(test)}.failed.png`
93
92
  }
94
93
  output.plugin('screenshotOnFail', 'Test failed, try to save a screenshot')
95
94
 
@@ -6,7 +6,7 @@ const defaultConfig = {
6
6
 
7
7
  module.exports = function (config) {
8
8
  config = Object.assign(defaultConfig, config)
9
- console.log(`Deprecation Warning: 'tryTo' has been moved to the 'codeceptjs/effects' module`)
9
+ console.log(`Deprecation Warning: 'tryTo' has been moved to the 'codeceptjs/effects' module. Disable tryTo plugin to remove this warning.`)
10
10
 
11
11
  if (config.registerGlobal) {
12
12
  global.tryTo = tryTo
package/lib/recorder.js CHANGED
@@ -192,6 +192,7 @@ module.exports = {
192
192
  .pop()
193
193
  // no retries or unnamed tasks
194
194
  debug(`${currentQueue()} Running | ${taskName} | Timeout: ${timeout || 'None'}`)
195
+ if (retryOpts) debug(`${currentQueue()} Retry opts`, JSON.stringify(retryOpts))
195
196
 
196
197
  if (!retryOpts || !taskName || !retry) {
197
198
  const [promise, timer] = getTimeoutPromise(timeout, taskName)
package/lib/store.js CHANGED
@@ -3,17 +3,41 @@
3
3
  * @namespace
4
4
  */
5
5
  const store = {
6
- /** @type {boolean} */
6
+ /**
7
+ * If we are in --debug mode
8
+ * @type {boolean}
9
+ */
7
10
  debugMode: false,
8
- /** @type {boolean} */
11
+
12
+ /**
13
+ * Is timeouts enabled
14
+ * @type {boolean}
15
+ */
9
16
  timeouts: true,
10
- /** @type {boolean} */
17
+
18
+ /**
19
+ * If auto-retries are enabled by retryFailedStep plugin
20
+ * tryTo effect disables them
21
+ * @type {boolean}
22
+ */
23
+ autoRetries: false,
24
+
25
+ /**
26
+ * Tests are executed via dry-run
27
+ * @type {boolean}
28
+ */
11
29
  dryRun: false,
12
- /** @type {boolean} */
30
+ /**
31
+ * If we are in pause mode
32
+ * @type {boolean}
33
+ */
13
34
  onPause: false,
35
+
36
+ // current object states
37
+
14
38
  /** @type {CodeceptJS.Test | null} */
15
39
  currentTest: null,
16
- /** @type {any} */
40
+ /** @type {CodeceptJS.Step | null} */
17
41
  currentStep: null,
18
42
  /** @type {CodeceptJS.Suite | null} */
19
43
  currentSuite: null,
package/lib/utils.js CHANGED
@@ -13,7 +13,7 @@ function deepMerge(target, source) {
13
13
  }
14
14
 
15
15
  module.exports.genTestId = test => {
16
- return require('crypto').createHash('sha256').update(test.fullTitle()).digest('base64').slice(0, -2)
16
+ return this.clearString(require('crypto').createHash('sha256').update(test.fullTitle()).digest('base64').slice(0, -2))
17
17
  }
18
18
 
19
19
  module.exports.deepMerge = deepMerge
@@ -94,19 +94,19 @@ module.exports.template = function (template, data) {
94
94
  /**
95
95
  * Make first char uppercase.
96
96
  * @param {string} str
97
- * @returns {string}
97
+ * @returns {string | undefined}
98
98
  */
99
99
  module.exports.ucfirst = function (str) {
100
- return str.charAt(0).toUpperCase() + str.substr(1)
100
+ if (str) return str.charAt(0).toUpperCase() + str.substr(1)
101
101
  }
102
102
 
103
103
  /**
104
104
  * Make first char lowercase.
105
105
  * @param {string} str
106
- * @returns {string}
106
+ * @returns {string | undefined}
107
107
  */
108
108
  module.exports.lcfirst = function (str) {
109
- return str.charAt(0).toLowerCase() + str.substr(1)
109
+ if (str) return str.charAt(0).toLowerCase() + str.substr(1)
110
110
  }
111
111
 
112
112
  module.exports.chunkArray = function (arr, chunk) {
package/lib/within.js CHANGED
@@ -7,6 +7,8 @@ const MetaStep = require('./step/meta')
7
7
  const { isAsyncFunction } = require('./utils')
8
8
 
9
9
  /**
10
+ * TODO: move to effects
11
+ *
10
12
  * @param {CodeceptJS.LocatorOrString} context
11
13
  * @param {Function} fn
12
14
  * @return {Promise<*> | undefined}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codeceptjs",
3
- "version": "3.7.0-rc.1",
3
+ "version": "3.7.1-beta.1",
4
4
  "description": "Supercharged End 2 End Testing Framework for NodeJS",
5
5
  "keywords": [
6
6
  "acceptance",
@@ -98,7 +98,7 @@
98
98
  "glob": ">=9.0.0 <12",
99
99
  "fuse.js": "^7.0.0",
100
100
  "html-minifier-terser": "7.2.0",
101
- "inquirer": "6.5.2",
101
+ "inquirer": "8.2.6",
102
102
  "invisi-data": "^1.0.0",
103
103
  "joi": "17.13.3",
104
104
  "js-beautify": "1.15.1",