codeceptjs 3.7.0-beta.4 → 3.7.0-beta.6

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 +9 -10
  2. package/bin/codecept.js +7 -0
  3. package/lib/actor.js +47 -92
  4. package/lib/command/check.js +173 -0
  5. package/lib/command/definitions.js +2 -0
  6. package/lib/command/run-workers.js +1 -1
  7. package/lib/command/workers/runTests.js +112 -109
  8. package/lib/container.js +9 -0
  9. package/lib/effects.js +218 -0
  10. package/lib/heal.js +10 -0
  11. package/lib/helper/Appium.js +27 -16
  12. package/lib/helper/Playwright.js +15 -0
  13. package/lib/helper/Puppeteer.js +5 -0
  14. package/lib/helper/WebDriver.js +9 -1
  15. package/lib/listener/emptyRun.js +2 -5
  16. package/lib/listener/globalTimeout.js +15 -3
  17. package/lib/listener/steps.js +3 -0
  18. package/lib/mocha/cli.js +22 -5
  19. package/lib/mocha/featureConfig.js +13 -0
  20. package/lib/mocha/scenarioConfig.js +11 -0
  21. package/lib/mocha/test.js +15 -0
  22. package/lib/mocha/types.d.ts +6 -0
  23. package/lib/output.js +74 -73
  24. package/lib/pause.js +3 -7
  25. package/lib/plugin/heal.js +30 -0
  26. package/lib/plugin/retryTo.js +18 -126
  27. package/lib/plugin/stepTimeout.js +1 -1
  28. package/lib/plugin/tryTo.js +13 -111
  29. package/lib/recorder.js +1 -1
  30. package/lib/step/base.js +180 -0
  31. package/lib/step/config.js +50 -0
  32. package/lib/step/helper.js +47 -0
  33. package/lib/step/meta.js +91 -0
  34. package/lib/step/record.js +74 -0
  35. package/lib/step/retry.js +11 -0
  36. package/lib/step/timeout.js +42 -0
  37. package/lib/step.js +15 -348
  38. package/lib/steps.js +23 -0
  39. package/lib/store.js +2 -0
  40. package/lib/utils.js +58 -0
  41. package/lib/within.js +2 -2
  42. package/lib/workers.js +2 -12
  43. package/package.json +9 -7
  44. package/typings/index.d.ts +5 -4
  45. package/typings/promiseBasedTypes.d.ts +520 -6
  46. package/typings/types.d.ts +562 -44
  47. package/lib/step/section.js +0 -25
@@ -386,7 +386,7 @@ class Appium extends Webdriver {
386
386
  _buildAppiumEndpoint() {
387
387
  const { protocol, port, hostname, path } = this.browser.options
388
388
  // Build path to Appium REST API endpoint
389
- return `${protocol}://${hostname}:${port}${path}`
389
+ return `${protocol}://${hostname}:${port}${path}/session/${this.browser.sessionId}`
390
390
  }
391
391
 
392
392
  /**
@@ -602,7 +602,7 @@ class Appium extends Webdriver {
602
602
 
603
603
  return this.axios({
604
604
  method: 'post',
605
- url: `${this._buildAppiumEndpoint()}/session/${this.browser.sessionId}/appium/device/remove_app`,
605
+ url: `${this._buildAppiumEndpoint()}/appium/device/remove_app`,
606
606
  data: { appId, bundleId },
607
607
  })
608
608
  }
@@ -619,7 +619,7 @@ class Appium extends Webdriver {
619
619
  onlyForApps.call(this)
620
620
  return this.axios({
621
621
  method: 'post',
622
- url: `${this._buildAppiumEndpoint()}/session/${this.browser.sessionId}/appium/app/reset`,
622
+ url: `${this._buildAppiumEndpoint()}/appium/app/reset`,
623
623
  })
624
624
  }
625
625
 
@@ -693,7 +693,7 @@ class Appium extends Webdriver {
693
693
 
694
694
  const res = await this.axios({
695
695
  method: 'get',
696
- url: `${this._buildAppiumEndpoint()}/session/${this.browser.sessionId}/orientation`,
696
+ url: `${this._buildAppiumEndpoint()}/orientation`,
697
697
  })
698
698
 
699
699
  const currentOrientation = res.data.value
@@ -717,7 +717,7 @@ class Appium extends Webdriver {
717
717
 
718
718
  return this.axios({
719
719
  method: 'post',
720
- url: `${this._buildAppiumEndpoint()}/session/${this.browser.sessionId}/orientation`,
720
+ url: `${this._buildAppiumEndpoint()}/orientation`,
721
721
  data: { orientation },
722
722
  })
723
723
  }
@@ -956,21 +956,19 @@ class Appium extends Webdriver {
956
956
  * ```js
957
957
  * // taps outside to hide keyboard per default
958
958
  * I.hideDeviceKeyboard();
959
- * I.hideDeviceKeyboard('tapOutside');
960
- *
961
- * // or by pressing key
962
- * I.hideDeviceKeyboard('pressKey', 'Done');
963
959
  * ```
964
960
  *
965
961
  * Appium: support Android and iOS
966
962
  *
967
- * @param {'tapOutside' | 'pressKey'} [strategy] Desired strategy to close keyboard (‘tapOutside’ or ‘pressKey’)
968
- * @param {string} [key] Optional key
969
963
  */
970
- async hideDeviceKeyboard(strategy, key) {
964
+ async hideDeviceKeyboard() {
971
965
  onlyForApps.call(this)
972
- strategy = strategy || 'tapOutside'
973
- return this.browser.hideKeyboard(strategy, key)
966
+
967
+ return this.axios({
968
+ method: 'post',
969
+ url: `${this._buildAppiumEndpoint()}/appium/device/hide_keyboard`,
970
+ data: {},
971
+ })
974
972
  }
975
973
 
976
974
  /**
@@ -1046,7 +1044,13 @@ class Appium extends Webdriver {
1046
1044
  * @param {*} locator
1047
1045
  */
1048
1046
  async tap(locator) {
1049
- return this.makeTouchAction(locator, 'tap')
1047
+ const { elementId } = await this.browser.$(parseLocator.call(this, locator))
1048
+
1049
+ return this.axios({
1050
+ method: 'post',
1051
+ url: `${this._buildAppiumEndpoint()}/element/${elementId}/click`,
1052
+ data: {},
1053
+ })
1050
1054
  }
1051
1055
 
1052
1056
  /**
@@ -1493,7 +1497,14 @@ class Appium extends Webdriver {
1493
1497
  */
1494
1498
  async click(locator, context) {
1495
1499
  if (this.isWeb) return super.click(locator, context)
1496
- return super.click(parseLocator.call(this, locator), parseLocator.call(this, context))
1500
+
1501
+ const { elementId } = await this.browser.$(parseLocator.call(this, locator), parseLocator.call(this, context))
1502
+
1503
+ return this.axios({
1504
+ method: 'post',
1505
+ url: `${this._buildAppiumEndpoint()}/element/${elementId}/click`,
1506
+ data: {},
1507
+ })
1497
1508
  }
1498
1509
 
1499
1510
  /**
@@ -482,6 +482,7 @@ class Playwright extends Helper {
482
482
 
483
483
  async _before(test) {
484
484
  this.currentRunningTest = test
485
+
485
486
  recorder.retry({
486
487
  retries: process.env.FAILED_STEP_RETRIES || 3,
487
488
  when: err => {
@@ -552,6 +553,15 @@ class Playwright extends Helper {
552
553
 
553
554
  await this._setPage(mainPage)
554
555
 
556
+ try {
557
+ // set metadata for reporting
558
+ test.meta.browser = this.browser.browserType().name()
559
+ test.meta.browserVersion = this.browser.version()
560
+ test.meta.windowSize = `${this.page.viewportSize().width}x${this.page.viewportSize().height}`
561
+ } catch (e) {
562
+ this.debug('Failed to set metadata for reporting')
563
+ }
564
+
555
565
  if (this.options.trace) await this.browserContext.tracing.start({ screenshots: true, snapshots: true })
556
566
 
557
567
  return this.browser
@@ -3499,6 +3509,11 @@ async function proceedSee(assertType, text, context, strict = false) {
3499
3509
  allText = await Promise.all(els.map(el => el.innerText()))
3500
3510
  }
3501
3511
 
3512
+ if (store?.currentStep?.opts?.ignoreCase === true) {
3513
+ text = text.toLowerCase()
3514
+ allText = allText.map(elText => elText.toLowerCase())
3515
+ }
3516
+
3502
3517
  if (strict) {
3503
3518
  return allText.map(elText => equals(description)[assertType](text, elText))
3504
3519
  }
@@ -2769,6 +2769,11 @@ async function proceedSee(assertType, text, context, strict = false) {
2769
2769
  allText = await Promise.all(els.map(el => el.getProperty('innerText').then(p => p.jsonValue())))
2770
2770
  }
2771
2771
 
2772
+ if (store?.currentStep?.opts?.ignoreCase === true) {
2773
+ text = text.toLowerCase()
2774
+ allText = allText.map(elText => elText.toLowerCase())
2775
+ }
2776
+
2772
2777
  if (strict) {
2773
2778
  return allText.map(elText => equals(description)[assertType](text, elText))
2774
2779
  }
@@ -7,6 +7,7 @@ const Helper = require('@codeceptjs/helper')
7
7
  const promiseRetry = require('promise-retry')
8
8
  const stringIncludes = require('../assert/include').includes
9
9
  const { urlEquals, equals } = require('../assert/equal')
10
+ const store = require('../store')
10
11
  const { debug } = require('../output')
11
12
  const { empty } = require('../assert/empty')
12
13
  const { truth } = require('../assert/truth')
@@ -2698,7 +2699,14 @@ async function proceedSee(assertType, text, context, strict = false) {
2698
2699
  const smartWaitEnabled = assertType === 'assert'
2699
2700
  const res = await this._locate(withStrictLocator(context), smartWaitEnabled)
2700
2701
  assertElementExists(res, context)
2701
- const selected = await forEachAsync(res, async el => this.browser.getElementText(getElementId(el)))
2702
+ let selected = await forEachAsync(res, async el => this.browser.getElementText(getElementId(el)))
2703
+
2704
+ // apply ignoreCase option
2705
+ if (store?.currentStep?.opts?.ignoreCase === true) {
2706
+ text = text.toLowerCase()
2707
+ selected = selected.map(elText => elText.toLowerCase())
2708
+ }
2709
+
2702
2710
  if (strict) {
2703
2711
  if (Array.isArray(selected) && selected.length !== 0) {
2704
2712
  return selected.map(elText => equals(description)[assertType](text, elText))
@@ -2,6 +2,7 @@ const figures = require('figures')
2
2
  const Container = require('../container')
3
3
  const event = require('../event')
4
4
  const output = require('../output')
5
+ const { searchWithFusejs } = require('../utils')
5
6
 
6
7
  module.exports = function () {
7
8
  let isEmptyRun = true
@@ -15,8 +16,6 @@ module.exports = function () {
15
16
  const mocha = Container.mocha()
16
17
 
17
18
  if (mocha.options.grep) {
18
- const Fuse = require('fuse.js')
19
-
20
19
  output.print()
21
20
  output.print('No tests found by pattern: ' + mocha.options.grep)
22
21
 
@@ -27,14 +26,12 @@ module.exports = function () {
27
26
  })
28
27
  })
29
28
 
30
- const fuse = new Fuse(allTests, {
29
+ const results = searchWithFusejs(allTests, mocha.options.grep.toString(), {
31
30
  includeScore: true,
32
31
  threshold: 0.6,
33
32
  caseSensitive: false,
34
33
  })
35
34
 
36
- const results = fuse.search(mocha.options.grep.toString())
37
-
38
35
  if (results.length > 0) {
39
36
  output.print()
40
37
  output.print('Maybe you wanted to run one of these tests?')
@@ -2,9 +2,9 @@ const event = require('../event')
2
2
  const output = require('../output')
3
3
  const recorder = require('../recorder')
4
4
  const Config = require('../config')
5
- const { timeouts } = require('../store')
5
+ const store = require('../store')
6
6
  const debug = require('debug')('codeceptjs:timeout')
7
- const TIMEOUT_ORDER = require('../step').TIMEOUT_ORDER
7
+ const { TIMEOUT_ORDER } = require('../step/timeout')
8
8
  const { BeforeSuiteHook, AfterSuiteHook } = require('../mocha/hooks')
9
9
 
10
10
  module.exports = function () {
@@ -13,7 +13,7 @@ module.exports = function () {
13
13
  let currentTest
14
14
  let currentTimeout
15
15
 
16
- if (!timeouts) {
16
+ if (!store.timeouts) {
17
17
  console.log('Timeouts were disabled')
18
18
  return
19
19
  }
@@ -22,9 +22,11 @@ module.exports = function () {
22
22
  // add separate configs to them?
23
23
  event.dispatcher.on(event.hook.started, hook => {
24
24
  if (hook instanceof BeforeSuiteHook) {
25
+ timeout = null
25
26
  suiteTimeout = []
26
27
  }
27
28
  if (hook instanceof AfterSuiteHook) {
29
+ timeout = null
28
30
  suiteTimeout = []
29
31
  }
30
32
  })
@@ -103,6 +105,11 @@ module.exports = function () {
103
105
  event.dispatcher.on(event.step.before, step => {
104
106
  if (typeof timeout !== 'number') return
105
107
 
108
+ if (!store.timeouts) {
109
+ debug('step', step.toCode().trim(), 'timeout disabled')
110
+ return
111
+ }
112
+
106
113
  if (timeout < 0) {
107
114
  debug('Previous steps timed out, setting timeout to 0.01s')
108
115
  step.setTimeout(0.01, TIMEOUT_ORDER.testOrSuite)
@@ -113,6 +120,11 @@ module.exports = function () {
113
120
  })
114
121
 
115
122
  event.dispatcher.on(event.step.finished, step => {
123
+ if (!store.timeouts) {
124
+ debug('step', step.toCode().trim(), 'timeout disabled')
125
+ return
126
+ }
127
+
116
128
  if (typeof timeout === 'number' && !Number.isNaN(timeout)) timeout -= step.duration
117
129
 
118
130
  if (typeof timeout === 'number' && timeout <= 0 && recorder.isRunning()) {
@@ -73,6 +73,7 @@ module.exports = function () {
73
73
  event.dispatcher.on(event.step.started, step => {
74
74
  step.startedAt = +new Date()
75
75
  step.test = currentTest
76
+ store.currentStep = step
76
77
  if (currentHook && Array.isArray(currentHook.steps)) {
77
78
  return currentHook.steps.push(step)
78
79
  }
@@ -84,5 +85,7 @@ module.exports = function () {
84
85
  step.finishedAt = +new Date()
85
86
  if (step.startedAt) step.duration = step.finishedAt - step.startedAt
86
87
  debug(`Step '${step}' finished; Duration: ${step.duration || 0}ms`)
88
+ store.currentStep = null
89
+ store.stepOptions = null
87
90
  })
88
91
  }
package/lib/mocha/cli.js CHANGED
@@ -1,6 +1,7 @@
1
1
  const {
2
2
  reporters: { Base },
3
3
  } = require('mocha')
4
+ const figures = require('figures')
4
5
  const ms = require('ms')
5
6
  const event = require('../event')
6
7
  const AssertionFailedError = require('../assert/error')
@@ -163,33 +164,49 @@ class Cli extends Base {
163
164
  const err = test.err
164
165
 
165
166
  let log = ''
167
+ let originalMessage = err.message
166
168
 
167
169
  if (err instanceof AssertionFailedError) {
168
170
  err.message = err.inspect()
169
171
  }
170
172
 
173
+ // multi-line error messages
174
+ err.message = '\n ' + (err.message || '').replace(/^/gm, ' ').trim()
175
+
171
176
  const steps = test.steps || (test.ctx && test.ctx.test.steps)
172
177
 
173
178
  if (steps && steps.length) {
174
179
  let scenarioTrace = ''
175
180
  steps.reverse().forEach(step => {
176
- const line = `- ${step.toCode()} ${step.line()}`
177
- // if (step.status === 'failed') line = '' + line;
181
+ const hasFailed = step.status === 'failed'
182
+ let line = `${hasFailed ? output.styles.bold(figures.cross) : figures.tick} ${step.toCode()} ${step.line()}`
183
+ if (hasFailed) line = output.styles.bold(line)
178
184
  scenarioTrace += `\n${line}`
179
185
  })
180
- log += `${output.styles.bold('Scenario Steps')}:${scenarioTrace}\n`
186
+ log += `${output.styles.basic(figures.circle)} ${output.styles.section('Scenario Steps')}:${scenarioTrace}\n`
181
187
  }
182
188
 
183
189
  // display artifacts in debug mode
184
190
  if (test?.artifacts && Object.keys(test.artifacts).length) {
185
- log += `\n${output.styles.bold('Artifacts:')}`
191
+ log += `\n${output.styles.basic(figures.circle)} ${output.styles.section('Artifacts:')}`
186
192
  for (const artifact of Object.keys(test.artifacts)) {
187
193
  log += `\n- ${artifact}: ${test.artifacts[artifact]}`
188
194
  }
189
195
  }
190
196
 
197
+ // display metadata
198
+ if (test.meta && Object.keys(test.meta).length) {
199
+ log += `\n\n${output.styles.basic(figures.circle)} ${output.styles.section('Metadata:')}`
200
+ for (const [key, value] of Object.entries(test.meta)) {
201
+ log += `\n- ${key}: ${value}`
202
+ }
203
+ }
204
+
191
205
  try {
192
- let stack = err.stack ? err.stack.split('\n') : []
206
+ let stack = err.stack
207
+ stack = stack.replace(originalMessage, '')
208
+ stack = stack ? stack.split('\n') : []
209
+
193
210
  if (stack[0] && stack[0].includes(err.message)) {
194
211
  stack.shift()
195
212
  }
@@ -7,6 +7,19 @@ class FeatureConfig {
7
7
  this.suite = suite
8
8
  }
9
9
 
10
+ /**
11
+ * Set metadata for this suite
12
+ * @param {string} key
13
+ * @param {string} value
14
+ * @returns {this}
15
+ */
16
+ meta(key, value) {
17
+ this.suite.tests.forEach(test => {
18
+ test.meta[key] = value
19
+ })
20
+ return this
21
+ }
22
+
10
23
  /**
11
24
  * Retry this test for number of times
12
25
  *
@@ -42,6 +42,17 @@ class ScenarioConfig {
42
42
  return this
43
43
  }
44
44
 
45
+ /**
46
+ * Set metadata for this test
47
+ * @param {string} key
48
+ * @param {string} value
49
+ * @returns {this}
50
+ */
51
+ meta(key, value) {
52
+ this.test.meta[key] = value
53
+ return this
54
+ }
55
+
45
56
  /**
46
57
  * Set timeout for this test
47
58
  * @param {number} timeout
package/lib/mocha/test.js CHANGED
@@ -1,4 +1,5 @@
1
1
  const Test = require('mocha/lib/test')
2
+ const Suite = require('mocha/lib/suite')
2
3
  const { test: testWrapper } = require('./asyncWrapper')
3
4
  const { enhanceMochaSuite } = require('./suite')
4
5
  const { genTestId } = require('../utils')
@@ -31,6 +32,12 @@ function enhanceMochaTest(test) {
31
32
  test.artifacts = []
32
33
  test.inject = {}
33
34
  test.opts = {}
35
+ test.meta = {}
36
+
37
+ test.notes = []
38
+ test.addNote = (type, note) => {
39
+ test.notes.push({ type, text: note })
40
+ }
34
41
 
35
42
  // Add new methods
36
43
  /**
@@ -47,6 +54,7 @@ function enhanceMochaTest(test) {
47
54
  test.applyOptions = function (opts) {
48
55
  if (!opts) opts = {}
49
56
  test.opts = opts
57
+ test.meta = opts.meta || {}
50
58
  test.totalTimeout = opts.timeout
51
59
  if (opts.retries) this.retries(opts.retries)
52
60
  }
@@ -54,7 +62,14 @@ function enhanceMochaTest(test) {
54
62
  return test
55
63
  }
56
64
 
65
+ function repackTestForWorkersTransport(test) {
66
+ test = Object.assign(new Test(test.title || '', () => {}), test)
67
+ test.parent = Object.assign(new Suite(test.parent.title), test.parent)
68
+ return test
69
+ }
70
+
57
71
  module.exports = {
58
72
  createTest,
59
73
  enhanceMochaTest,
74
+ repackTestForWorkersTransport,
60
75
  }
@@ -7,6 +7,11 @@ declare global {
7
7
  title: string
8
8
  tags: string[]
9
9
  steps: string[]
10
+ meta: Record<string, any>
11
+ notes: Array<{
12
+ type: string
13
+ text: string
14
+ }>
10
15
  config: Record<string, any>
11
16
  artifacts: string[]
12
17
  inject: Record<string, any>
@@ -15,6 +20,7 @@ declare global {
15
20
  totalTimeout?: number
16
21
  addToSuite(suite: Mocha.Suite): void
17
22
  applyOptions(opts: Record<string, any>): void
23
+ addNote(type: string, note: string): void
18
24
  codeceptjs: boolean
19
25
  }
20
26