codeceptjs 3.7.0-beta.8 → 3.7.0-rc.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.
Files changed (46) hide show
  1. package/lib/codecept.js +14 -12
  2. package/lib/command/check.js +11 -2
  3. package/lib/command/definitions.js +1 -1
  4. package/lib/command/gherkin/snippets.js +69 -69
  5. package/lib/command/interactive.js +1 -1
  6. package/lib/command/run-multiple/chunk.js +48 -45
  7. package/lib/container.js +14 -7
  8. package/lib/els.js +87 -106
  9. package/lib/helper/AI.js +2 -1
  10. package/lib/helper/Playwright.js +7 -1
  11. package/lib/listener/store.js +9 -1
  12. package/lib/mocha/asyncWrapper.js +6 -4
  13. package/lib/mocha/cli.js +15 -7
  14. package/lib/mocha/gherkin.js +1 -1
  15. package/lib/mocha/inject.js +5 -0
  16. package/lib/output.js +1 -1
  17. package/lib/plugin/analyze.js +14 -17
  18. package/lib/plugin/auth.js +435 -0
  19. package/lib/plugin/autoDelay.js +2 -2
  20. package/lib/plugin/autoLogin.js +3 -337
  21. package/lib/plugin/pageInfo.js +1 -4
  22. package/lib/plugin/retryTo.js +6 -17
  23. package/lib/plugin/screenshotOnFail.js +8 -11
  24. package/lib/plugin/standardActingHelpers.js +4 -1
  25. package/lib/plugin/stepByStepReport.js +6 -5
  26. package/lib/plugin/tryTo.js +6 -15
  27. package/lib/step/base.js +15 -4
  28. package/lib/step/comment.js +10 -0
  29. package/lib/step/func.js +46 -0
  30. package/lib/step.js +6 -0
  31. package/lib/store.js +2 -0
  32. package/package.json +21 -20
  33. package/translations/de-DE.js +4 -3
  34. package/translations/fr-FR.js +4 -3
  35. package/translations/index.js +1 -0
  36. package/translations/it-IT.js +4 -3
  37. package/translations/ja-JP.js +4 -3
  38. package/translations/nl-NL.js +76 -0
  39. package/translations/pl-PL.js +4 -3
  40. package/translations/pt-BR.js +4 -3
  41. package/translations/ru-RU.js +4 -3
  42. package/translations/utils.js +9 -0
  43. package/translations/zh-CN.js +4 -3
  44. package/translations/zh-TW.js +4 -3
  45. package/typings/promiseBasedTypes.d.ts +0 -18
  46. package/typings/types.d.ts +10 -18
package/lib/els.js CHANGED
@@ -1,115 +1,124 @@
1
- const output = require('./output');
2
- const store = require('./store');
3
- const recorder = require('./recorder');
4
- const container = require('./container');
5
- const event = require('./event');
6
- const Step = require('./step');
7
- const { truth } = require('./assert/truth');
8
- const { isAsyncFunction, humanizeFunction } = require('./utils');
1
+ const output = require('./output')
2
+ const store = require('./store')
3
+ const container = require('./container')
4
+ const StepConfig = require('./step/config')
5
+ const recordStep = require('./step/record')
6
+ const FuncStep = require('./step/func')
7
+ const { truth } = require('./assert/truth')
8
+ const { isAsyncFunction, humanizeFunction } = require('./utils')
9
9
 
10
10
  function element(purpose, locator, fn) {
11
- if (!fn) {
12
- fn = locator;
13
- locator = purpose;
14
- purpose = 'first element';
11
+ let stepConfig
12
+ if (arguments[arguments.length - 1] instanceof StepConfig) {
13
+ stepConfig = arguments[arguments.length - 1]
15
14
  }
16
15
 
17
- const step = prepareStep(purpose, locator, fn);
18
- if (!step) return;
16
+ if (!fn || fn === stepConfig) {
17
+ fn = locator
18
+ locator = purpose
19
+ purpose = 'first element'
20
+ }
19
21
 
20
- return executeStep(step, async () => {
21
- const els = await step.helper._locate(locator);
22
- output.debug(`Found ${els.length} elements, using first element`);
22
+ const step = prepareStep(purpose, locator, fn)
23
+ if (!step) return
23
24
 
24
- return fn(els[0]);
25
- });
25
+ return executeStep(
26
+ step,
27
+ async () => {
28
+ const els = await step.helper._locate(locator)
29
+ output.debug(`Found ${els.length} elements, using first element`)
30
+
31
+ return fn(els[0])
32
+ },
33
+ stepConfig,
34
+ )
26
35
  }
27
36
 
28
37
  function eachElement(purpose, locator, fn) {
29
38
  if (!fn) {
30
- fn = locator;
31
- locator = purpose;
32
- purpose = 'for each element';
39
+ fn = locator
40
+ locator = purpose
41
+ purpose = 'for each element'
33
42
  }
34
43
 
35
- const step = prepareStep(purpose, locator, fn);
36
- if (!step) return;
44
+ const step = prepareStep(purpose, locator, fn)
45
+ if (!step) return
37
46
 
38
47
  return executeStep(step, async () => {
39
- const els = await step.helper._locate(locator);
40
- output.debug(`Found ${els.length} elements for each elements to iterate`);
48
+ const els = await step.helper._locate(locator)
49
+ output.debug(`Found ${els.length} elements for each elements to iterate`)
41
50
 
42
- const errs = [];
43
- let i = 0;
51
+ const errs = []
52
+ let i = 0
44
53
  for (const el of els) {
45
54
  try {
46
- await fn(el, i);
55
+ await fn(el, i)
47
56
  } catch (err) {
48
- output.error(`eachElement: failed operation on element #${i} ${el}`);
49
- errs.push(err);
57
+ output.error(`eachElement: failed operation on element #${i} ${el}`)
58
+ errs.push(err)
50
59
  }
51
- i++;
60
+ i++
52
61
  }
53
62
 
54
63
  if (errs.length) {
55
- throw errs[0];
64
+ throw errs[0]
56
65
  }
57
- });
66
+ })
58
67
  }
59
68
 
60
69
  function expectElement(locator, fn) {
61
- const step = prepareStep('expect element to be', locator, fn);
62
- if (!step) return;
70
+ const step = prepareStep('expect element to be', locator, fn)
71
+ if (!step) return
63
72
 
64
73
  return executeStep(step, async () => {
65
- const els = await step.helper._locate(locator);
66
- output.debug(`Found ${els.length} elements, first will be used for assertion`);
74
+ const els = await step.helper._locate(locator)
75
+ output.debug(`Found ${els.length} elements, first will be used for assertion`)
67
76
 
68
- const result = await fn(els[0]);
69
- const assertion = truth(`element (${locator})`, fn.toString());
70
- assertion.assert(result);
71
- });
77
+ const result = await fn(els[0])
78
+ const assertion = truth(`element (${locator})`, fn.toString())
79
+ assertion.assert(result)
80
+ })
72
81
  }
73
82
 
74
83
  function expectAnyElement(locator, fn) {
75
- const step = prepareStep('expect any element to be', locator, fn);
76
- if (!step) return;
84
+ const step = prepareStep('expect any element to be', locator, fn)
85
+ if (!step) return
77
86
 
78
87
  return executeStep(step, async () => {
79
- const els = await step.helper._locate(locator);
80
- output.debug(`Found ${els.length} elements, at least one should pass the assertion`);
88
+ const els = await step.helper._locate(locator)
89
+ output.debug(`Found ${els.length} elements, at least one should pass the assertion`)
81
90
 
82
- const assertion = truth(`any element of (${locator})`, fn.toString());
91
+ const assertion = truth(`any element of (${locator})`, fn.toString())
83
92
 
84
- let found = false;
93
+ let found = false
85
94
  for (const el of els) {
86
- const result = await fn(el);
95
+ const result = await fn(el)
87
96
  if (result) {
88
- found = true;
89
- break;
97
+ found = true
98
+ break
90
99
  }
91
100
  }
92
- if (!found) throw assertion.getException();
93
- });
101
+ if (!found) throw assertion.getException()
102
+ })
94
103
  }
95
104
 
96
105
  function expectAllElements(locator, fn) {
97
- const step = prepareStep('expect all elements', locator, fn);
98
- if (!step) return;
106
+ const step = prepareStep('expect all elements', locator, fn)
107
+ if (!step) return
99
108
 
100
109
  return executeStep(step, async () => {
101
- const els = await step.helper._locate(locator);
102
- output.debug(`Found ${els.length} elements, all should pass the assertion`);
110
+ const els = await step.helper._locate(locator)
111
+ output.debug(`Found ${els.length} elements, all should pass the assertion`)
103
112
 
104
- let i = 1;
113
+ let i = 1
105
114
  for (const el of els) {
106
- output.debug(`checking element #${i}: ${el}`);
107
- const result = await fn(el);
108
- const assertion = truth(`element #${i} of (${locator})`, humanizeFunction(fn));
109
- assertion.assert(result);
110
- i++;
115
+ output.debug(`checking element #${i}: ${el}`)
116
+ const result = await fn(el)
117
+ const assertion = truth(`element #${i} of (${locator})`, humanizeFunction(fn))
118
+ assertion.assert(result)
119
+ i++
111
120
  }
112
- });
121
+ })
113
122
  }
114
123
 
115
124
  module.exports = {
@@ -118,60 +127,32 @@ module.exports = {
118
127
  expectElement,
119
128
  expectAnyElement,
120
129
  expectAllElements,
121
- };
130
+ }
122
131
 
123
132
  function prepareStep(purpose, locator, fn) {
124
- if (store.dryRun) return;
125
- const helpers = Object.values(container.helpers());
133
+ if (store.dryRun) return
134
+ const helpers = Object.values(container.helpers())
126
135
 
127
- const helper = helpers.filter(h => !!h._locate)[0];
136
+ const helper = helpers.filter(h => !!h._locate)[0]
128
137
 
129
138
  if (!helper) {
130
- throw new Error('No helper enabled with _locate method with returns a list of elements.');
139
+ throw new Error('No helper enabled with _locate method with returns a list of elements.')
131
140
  }
132
141
 
133
142
  if (!isAsyncFunction(fn)) {
134
- throw new Error('Async function should be passed into each element');
143
+ throw new Error('Async function should be passed into each element')
135
144
  }
136
145
 
137
- const isAssertion = purpose.startsWith('expect');
146
+ const isAssertion = purpose.startsWith('expect')
138
147
 
139
- const step = new Step(helper, `${purpose} within "${locator}" ${isAssertion ? 'to be' : 'to'}`);
140
- step.setActor('EL');
141
- step.setArguments([humanizeFunction(fn)]);
142
- step.helperMethod = '_locate';
148
+ const step = new FuncStep(`${purpose} within "${locator}" ${isAssertion ? 'to be' : 'to'}`)
149
+ step.setHelper(helper)
150
+ step.setArguments([humanizeFunction(fn)]) // user defined function is a passed argument
143
151
 
144
- return step;
152
+ return step
145
153
  }
146
154
 
147
- async function executeStep(step, action) {
148
- let error;
149
- const promise = recorder.add('register element wrapper', async () => {
150
- event.emit(event.step.started, step);
151
-
152
- try {
153
- await action();
154
- } catch (err) {
155
- recorder.throw(err);
156
- event.emit(event.step.failed, step, err);
157
- event.emit(event.step.finished, step);
158
- // event.emit(event.step.after, step)
159
- error = err;
160
- // await recorder.promise();
161
- return;
162
- }
163
-
164
- event.emit(event.step.after, step);
165
- event.emit(event.step.passed, step);
166
- event.emit(event.step.finished, step);
167
- });
168
-
169
- // await recorder.promise();
170
-
171
- // if (error) {
172
- // console.log('error', error.inspect())
173
- // return recorder.throw(error);
174
- // }
175
-
176
- return promise;
155
+ async function executeStep(step, action, stepConfig = {}) {
156
+ step.setCallable(action)
157
+ return recordStep(step, [stepConfig])
177
158
  }
package/lib/helper/AI.js CHANGED
@@ -3,13 +3,14 @@ const ora = require('ora-classic')
3
3
  const fs = require('fs')
4
4
  const path = require('path')
5
5
  const ai = require('../ai')
6
- const standardActingHelpers = require('../plugin/standardActingHelpers')
7
6
  const Container = require('../container')
8
7
  const { splitByChunks, minifyHtml } = require('../html')
9
8
  const { beautify } = require('../utils')
10
9
  const output = require('../output')
11
10
  const { registerVariable } = require('../pause')
12
11
 
12
+ const standardActingHelpers = Container.STANDARD_ACTING_HELPERS
13
+
13
14
  const gtpRole = {
14
15
  user: 'user',
15
16
  }
@@ -523,12 +523,17 @@ class Playwright extends Helper {
523
523
  this.currentRunningTest.artifacts.har = fileName
524
524
  contextOptions.recordHar = this.options.recordHar
525
525
  }
526
+
527
+ // load pre-saved cookies
528
+ if (test?.opts?.cookies) contextOptions.storageState = { cookies: test.opts.cookies }
529
+
526
530
  if (this.storageState) contextOptions.storageState = this.storageState
527
531
  if (this.options.userAgent) contextOptions.userAgent = this.options.userAgent
528
532
  if (this.options.locale) contextOptions.locale = this.options.locale
529
533
  if (this.options.colorScheme) contextOptions.colorScheme = this.options.colorScheme
530
534
  this.contextOptions = contextOptions
531
535
  if (!this.browserContext || !restartsSession()) {
536
+ this.debugSection('New Session', JSON.stringify(this.contextOptions))
532
537
  this.browserContext = await this.browser.newContext(this.contextOptions) // Adding the HTTPSError ignore in the context so that we can ignore those errors
533
538
  }
534
539
  }
@@ -938,7 +943,8 @@ class Playwright extends Helper {
938
943
  throw new Error('Cannot open pages inside an Electron container')
939
944
  }
940
945
  if (!/^\w+\:(\/\/|.+)/.test(url)) {
941
- url = this.options.url + (url.startsWith('/') ? url : `/${url}`)
946
+ url = this.options.url + (!this.options.url.endsWith('/') && url.startsWith('/') ? url : `/${url}`)
947
+ this.debug(`Changed URL to base url + relative path: ${url}`)
942
948
  }
943
949
 
944
950
  if (this.options.basicAuth && this.isAuthenticated !== true) {
@@ -2,11 +2,19 @@ const event = require('../event')
2
2
  const store = require('../store')
3
3
 
4
4
  module.exports = function () {
5
+ event.dispatcher.on(event.suite.before, suite => {
6
+ store.currentSuite = suite
7
+ })
8
+
9
+ event.dispatcher.on(event.suite.after, () => {
10
+ store.currentSuite = null
11
+ })
12
+
5
13
  event.dispatcher.on(event.test.before, test => {
6
14
  store.currentTest = test
7
15
  })
8
16
 
9
- event.dispatcher.on(event.test.finished, test => {
17
+ event.dispatcher.on(event.test.finished, () => {
10
18
  store.currentTest = null
11
19
  })
12
20
  }
@@ -19,10 +19,10 @@ const injectHook = function (inject, suite) {
19
19
  return recorder.promise()
20
20
  }
21
21
 
22
- function suiteTestFailedHookError(suite, err) {
22
+ function suiteTestFailedHookError(suite, err, hookName) {
23
23
  suite.eachTest(test => {
24
24
  test.err = err
25
- event.emit(event.test.failed, test, err)
25
+ event.emit(event.test.failed, test, err, ucfirst(hookName))
26
26
  })
27
27
  }
28
28
 
@@ -120,7 +120,7 @@ module.exports.injected = function (fn, suite, hookName) {
120
120
  const errHandler = err => {
121
121
  recorder.session.start('teardown')
122
122
  recorder.cleanAsyncErr()
123
- if (hookName == 'before' || hookName == 'beforeSuite') suiteTestFailedHookError(suite, err)
123
+ if (hookName == 'before' || hookName == 'beforeSuite') suiteTestFailedHookError(suite, err, hookName)
124
124
  if (hookName === 'after') event.emit(event.test.after, suite)
125
125
  if (hookName === 'afterSuite') event.emit(event.suite.after, suite)
126
126
  recorder.add(() => doneFn(err))
@@ -145,11 +145,13 @@ module.exports.injected = function (fn, suite, hookName) {
145
145
  const opts = suite.opts || {}
146
146
  const retries = opts[`retry${ucfirst(hookName)}`] || 0
147
147
 
148
+ const currentTest = hookName === 'before' || hookName === 'after' ? suite?.ctx?.currentTest : null
149
+
148
150
  promiseRetry(
149
151
  async (retry, number) => {
150
152
  try {
151
153
  recorder.startUnlessRunning()
152
- await fn.call(this, getInjectedArguments(fn))
154
+ await fn.call(this, { ...getInjectedArguments(fn), suite, test: currentTest })
153
155
  await recorder.promise().catch(err => retry(err))
154
156
  } catch (err) {
155
157
  retry(err)
package/lib/mocha/cli.js CHANGED
@@ -198,17 +198,25 @@ class Cli extends Base {
198
198
  // add new line before the message
199
199
  err.message = '\n ' + err.message
200
200
 
201
+ // explicitly show file with error
202
+ if (test.file) {
203
+ log += `\n${output.styles.basic(figures.circle)} ${output.styles.section('File:')} ${output.styles.basic(test.file)}\n`
204
+ }
205
+
201
206
  const steps = test.steps || (test.ctx && test.ctx.test.steps)
202
207
 
203
208
  if (steps && steps.length) {
204
209
  let scenarioTrace = ''
205
- steps.reverse().forEach(step => {
206
- const hasFailed = step.status === 'failed'
207
- let line = `${hasFailed ? output.styles.bold(figures.cross) : figures.tick} ${step.toCode()} ${step.line()}`
208
- if (hasFailed) line = output.styles.bold(line)
209
- scenarioTrace += `\n${line}`
210
- })
211
- log += `${output.styles.basic(figures.circle)} ${output.styles.section('Scenario Steps')}:${scenarioTrace}\n`
210
+ steps
211
+ .reverse()
212
+ .slice(0, 10)
213
+ .forEach(step => {
214
+ const hasFailed = step.status === 'failed'
215
+ let line = `${hasFailed ? output.styles.bold(figures.cross) : figures.tick} ${step.toCode()} ${step.line()}`
216
+ if (hasFailed) line = output.styles.bold(line)
217
+ scenarioTrace += `\n${line}`
218
+ })
219
+ log += `\n${output.styles.basic(figures.circle)} ${output.styles.section('Scenario Steps')}:${scenarioTrace}\n`
212
220
  }
213
221
 
214
222
  // display artifacts in debug mode
@@ -107,7 +107,7 @@ module.exports = (text, file) => {
107
107
  )
108
108
  continue
109
109
  }
110
- if (child.scenario && (currentLanguage ? child.scenario.keyword === currentLanguage.contexts.ScenarioOutline : child.scenario.keyword === 'Scenario Outline')) {
110
+ if (child.scenario && (currentLanguage ? currentLanguage.contexts.ScenarioOutline.includes(child.scenario.keyword) : child.scenario.keyword === 'Scenario Outline')) {
111
111
  for (const examples of child.scenario.examples) {
112
112
  const fields = examples.tableHeader.cells.map(c => c.value)
113
113
  for (const example of examples.tableBody) {
@@ -5,6 +5,7 @@ const getInjectedArguments = (fn, test) => {
5
5
  const testArgs = {}
6
6
  const params = parser.getParams(fn) || []
7
7
  const objects = container.support()
8
+
8
9
  for (const key of params) {
9
10
  testArgs[key] = {}
10
11
  if (test && test.inject && test.inject[key]) {
@@ -18,6 +19,10 @@ const getInjectedArguments = (fn, test) => {
18
19
  testArgs[key] = container.support(key)
19
20
  }
20
21
 
22
+ if (test) {
23
+ testArgs.suite = test?.parent
24
+ testArgs.test = test
25
+ }
21
26
  return testArgs
22
27
  }
23
28
 
package/lib/output.js CHANGED
@@ -115,7 +115,7 @@ module.exports = {
115
115
  }
116
116
  }
117
117
 
118
- let stepLine = step.toCliStyled()
118
+ let stepLine = step.toCliStyled ? step.toCliStyled() : step.toString()
119
119
  if (step.metaStep && outputLevel >= 1) {
120
120
  // this.stepShift += 2;
121
121
  stepLine = colors.dim(truncate(stepLine, this.spaceShift))
@@ -2,6 +2,7 @@ const debug = require('debug')('codeceptjs:analyze')
2
2
  const { isMainThread } = require('node:worker_threads')
3
3
  const { arrowRight } = require('figures')
4
4
  const container = require('../container')
5
+ const store = require('../store')
5
6
  const ai = require('../ai')
6
7
  const colors = require('chalk')
7
8
  const ora = require('ora-classic')
@@ -12,8 +13,8 @@ const { ansiRegExp, base64EncodeFile, markdownToAnsi } = require('../utils')
12
13
  const MAX_DATA_LENGTH = 5000
13
14
 
14
15
  const defaultConfig = {
15
- clusterize: 2,
16
- analyze: 3,
16
+ clusterize: 5,
17
+ analyze: 2,
17
18
  vision: false,
18
19
  categories: [
19
20
  'Browser connection error / browser crash',
@@ -64,17 +65,19 @@ const defaultConfig = {
64
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.
65
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.
66
67
  Pick different emojis for each group.
67
- Do not include group into report if it has only one test in affected tests section.
68
+ Order groups by the number of tests in the group.
69
+ If group has one test, skip that group.
68
70
 
69
71
  Provide list of groups in following format:
70
72
 
71
73
  _______________________________
72
74
 
73
- ## Group <group_number>
75
+ ## Group <group_number> <emoji>
74
76
 
77
+ * SUMMARY <summary_of_errors>
75
78
  * CATEGORY <category_of_failure>
79
+ * URL <url_of_failure_if_any>
76
80
  * ERROR <error_message_1>, <error_message_2>, ...
77
- * SUMMARY <summary_of_errors>
78
81
  * STEP <step_of_failure> (use CodeceptJS format I.click(), I.see(), etc; if all failures happend on the same step)
79
82
  * SUITE <suite_title>, <suite_title> (if SUITE is present, and if all tests in the group have the same suite or suites)
80
83
  * TAG <tag> (if TAG is present, and if all tests in the group have the same tag)
@@ -85,11 +88,6 @@ const defaultConfig = {
85
88
  x ...
86
89
  `,
87
90
  },
88
- {
89
- role: 'assistant',
90
- content: `## '
91
- `,
92
- },
93
91
  ]
94
92
  return messages
95
93
  },
@@ -126,14 +124,17 @@ const defaultConfig = {
126
124
  Do not get to details, be concise.
127
125
  If there is failed step, just write it in STEPS section.
128
126
  If you have suggestions for the test, write them in SUMMARY section.
127
+ Do not be too technical in SUMMARY section.
129
128
  Inside SUMMARY write exact values, if you have suggestions, explain which information you used to suggest.
130
129
  Be concise, each section should not take more than one sentence.
131
130
 
132
131
  Response format:
133
132
 
133
+ * SUMMARY <explanation_of_failure>
134
+ * ERROR <error_message_1>, <error_message_2>, ...
134
135
  * CATEGORY <category_of_failure>
135
136
  * STEPS <step_of_failure>
136
- * SUMMARY <explanation_of_failure>
137
+ * URL <url_of_failure_if_any>
137
138
 
138
139
  Do not add any other sections or explanations. Only CATEGORY, SUMMARY, STEPS.
139
140
  ${config.vision ? 'Also a screenshot of the page is attached to the prompt.' : ''}
@@ -153,11 +154,6 @@ const defaultConfig = {
153
154
  })
154
155
  }
155
156
 
156
- messages.push({
157
- role: 'assistant',
158
- content: `## `,
159
- })
160
-
161
157
  return messages
162
158
  },
163
159
  },
@@ -340,12 +336,13 @@ function serializeTest(test) {
340
336
  }
341
337
 
342
338
  function formatResponse(response) {
343
- if (!response.startsWith('##')) response = '## ' + response
344
339
  return response
340
+ .replace(/<think>([\s\S]*?)<\/think>/g, store.debugMode ? colors.cyan('$1') : '')
345
341
  .split('\n')
346
342
  .map(line => line.trim())
347
343
  .filter(line => !/^[A-Z\s]+$/.test(line))
348
344
  .map(line => markdownToAnsi(line))
349
345
  .map(line => line.replace(/^x /gm, ` ${colors.red.bold('x')} `))
350
346
  .join('\n')
347
+ .trim()
351
348
  }