codeceptjs 4.0.0-beta.3 → 4.0.0-beta.5

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 (155) hide show
  1. package/README.md +134 -119
  2. package/bin/codecept.js +12 -2
  3. package/bin/test-server.js +53 -0
  4. package/docs/webapi/clearCookie.mustache +1 -1
  5. package/lib/actor.js +66 -102
  6. package/lib/ai.js +130 -121
  7. package/lib/assert/empty.js +3 -5
  8. package/lib/assert/equal.js +4 -7
  9. package/lib/assert/include.js +4 -6
  10. package/lib/assert/throws.js +2 -4
  11. package/lib/assert/truth.js +2 -2
  12. package/lib/codecept.js +141 -86
  13. package/lib/command/check.js +201 -0
  14. package/lib/command/configMigrate.js +2 -4
  15. package/lib/command/definitions.js +8 -26
  16. package/lib/command/dryRun.js +30 -35
  17. package/lib/command/generate.js +10 -14
  18. package/lib/command/gherkin/snippets.js +75 -73
  19. package/lib/command/gherkin/steps.js +1 -1
  20. package/lib/command/info.js +42 -8
  21. package/lib/command/init.js +13 -12
  22. package/lib/command/interactive.js +10 -2
  23. package/lib/command/list.js +1 -1
  24. package/lib/command/run-multiple/chunk.js +48 -45
  25. package/lib/command/run-multiple.js +12 -35
  26. package/lib/command/run-workers.js +21 -58
  27. package/lib/command/utils.js +5 -6
  28. package/lib/command/workers/runTests.js +263 -222
  29. package/lib/container.js +386 -238
  30. package/lib/data/context.js +10 -13
  31. package/lib/data/dataScenarioConfig.js +8 -8
  32. package/lib/data/dataTableArgument.js +6 -6
  33. package/lib/data/table.js +5 -11
  34. package/lib/effects.js +223 -0
  35. package/lib/element/WebElement.js +327 -0
  36. package/lib/els.js +158 -0
  37. package/lib/event.js +21 -17
  38. package/lib/heal.js +88 -80
  39. package/lib/helper/AI.js +2 -1
  40. package/lib/helper/ApiDataFactory.js +4 -7
  41. package/lib/helper/Appium.js +50 -57
  42. package/lib/helper/FileSystem.js +3 -3
  43. package/lib/helper/GraphQLDataFactory.js +4 -4
  44. package/lib/helper/JSONResponse.js +75 -37
  45. package/lib/helper/Mochawesome.js +31 -9
  46. package/lib/helper/Nightmare.js +37 -58
  47. package/lib/helper/Playwright.js +267 -272
  48. package/lib/helper/Protractor.js +56 -87
  49. package/lib/helper/Puppeteer.js +247 -264
  50. package/lib/helper/REST.js +29 -17
  51. package/lib/helper/TestCafe.js +22 -47
  52. package/lib/helper/WebDriver.js +157 -368
  53. package/lib/helper/extras/PlaywrightPropEngine.js +2 -2
  54. package/lib/helper/extras/Popup.js +22 -22
  55. package/lib/helper/network/utils.js +1 -1
  56. package/lib/helper/testcafe/testcafe-utils.js +27 -28
  57. package/lib/listener/emptyRun.js +55 -0
  58. package/lib/listener/exit.js +7 -10
  59. package/lib/listener/{retry.js → globalRetry.js} +5 -5
  60. package/lib/listener/globalTimeout.js +165 -0
  61. package/lib/listener/helpers.js +15 -15
  62. package/lib/listener/mocha.js +1 -1
  63. package/lib/listener/result.js +12 -0
  64. package/lib/listener/retryEnhancer.js +85 -0
  65. package/lib/listener/steps.js +32 -18
  66. package/lib/listener/store.js +20 -0
  67. package/lib/locator.js +1 -1
  68. package/lib/mocha/asyncWrapper.js +231 -0
  69. package/lib/{interfaces → mocha}/bdd.js +3 -3
  70. package/lib/mocha/cli.js +308 -0
  71. package/lib/mocha/factory.js +104 -0
  72. package/lib/{interfaces → mocha}/featureConfig.js +32 -12
  73. package/lib/{interfaces → mocha}/gherkin.js +26 -28
  74. package/lib/mocha/hooks.js +112 -0
  75. package/lib/mocha/index.js +12 -0
  76. package/lib/mocha/inject.js +29 -0
  77. package/lib/{interfaces → mocha}/scenarioConfig.js +31 -7
  78. package/lib/mocha/suite.js +82 -0
  79. package/lib/mocha/test.js +181 -0
  80. package/lib/mocha/types.d.ts +42 -0
  81. package/lib/mocha/ui.js +232 -0
  82. package/lib/output.js +93 -65
  83. package/lib/pause.js +160 -138
  84. package/lib/plugin/analyze.js +396 -0
  85. package/lib/plugin/auth.js +435 -0
  86. package/lib/plugin/autoDelay.js +8 -8
  87. package/lib/plugin/autoLogin.js +3 -338
  88. package/lib/plugin/commentStep.js +6 -1
  89. package/lib/plugin/coverage.js +10 -22
  90. package/lib/plugin/customLocator.js +3 -3
  91. package/lib/plugin/customReporter.js +52 -0
  92. package/lib/plugin/eachElement.js +1 -1
  93. package/lib/plugin/fakerTransform.js +1 -1
  94. package/lib/plugin/heal.js +36 -9
  95. package/lib/plugin/htmlReporter.js +1947 -0
  96. package/lib/plugin/pageInfo.js +140 -0
  97. package/lib/plugin/retryFailedStep.js +17 -18
  98. package/lib/plugin/retryTo.js +2 -113
  99. package/lib/plugin/screenshotOnFail.js +17 -58
  100. package/lib/plugin/selenoid.js +15 -35
  101. package/lib/plugin/standardActingHelpers.js +4 -1
  102. package/lib/plugin/stepByStepReport.js +56 -17
  103. package/lib/plugin/stepTimeout.js +5 -12
  104. package/lib/plugin/subtitles.js +4 -4
  105. package/lib/plugin/tryTo.js +3 -102
  106. package/lib/plugin/wdio.js +8 -10
  107. package/lib/recorder.js +155 -124
  108. package/lib/rerun.js +43 -42
  109. package/lib/result.js +161 -0
  110. package/lib/secret.js +1 -2
  111. package/lib/step/base.js +239 -0
  112. package/lib/step/comment.js +10 -0
  113. package/lib/step/config.js +50 -0
  114. package/lib/step/func.js +46 -0
  115. package/lib/step/helper.js +50 -0
  116. package/lib/step/meta.js +99 -0
  117. package/lib/step/record.js +74 -0
  118. package/lib/step/retry.js +11 -0
  119. package/lib/step/section.js +55 -0
  120. package/lib/step.js +21 -332
  121. package/lib/steps.js +50 -0
  122. package/lib/store.js +37 -5
  123. package/lib/template/heal.js +2 -11
  124. package/lib/test-server.js +323 -0
  125. package/lib/timeout.js +66 -0
  126. package/lib/utils.js +351 -218
  127. package/lib/within.js +75 -55
  128. package/lib/workerStorage.js +2 -1
  129. package/lib/workers.js +386 -277
  130. package/package.json +81 -75
  131. package/translations/de-DE.js +5 -3
  132. package/translations/fr-FR.js +5 -4
  133. package/translations/index.js +1 -0
  134. package/translations/it-IT.js +4 -3
  135. package/translations/ja-JP.js +4 -3
  136. package/translations/nl-NL.js +76 -0
  137. package/translations/pl-PL.js +4 -3
  138. package/translations/pt-BR.js +4 -3
  139. package/translations/ru-RU.js +4 -3
  140. package/translations/utils.js +9 -0
  141. package/translations/zh-CN.js +4 -3
  142. package/translations/zh-TW.js +4 -3
  143. package/typings/index.d.ts +197 -187
  144. package/typings/promiseBasedTypes.d.ts +53 -903
  145. package/typings/types.d.ts +372 -1042
  146. package/lib/cli.js +0 -257
  147. package/lib/helper/ExpectHelper.js +0 -391
  148. package/lib/helper/MockServer.js +0 -221
  149. package/lib/helper/SoftExpectHelper.js +0 -381
  150. package/lib/listener/artifacts.js +0 -19
  151. package/lib/listener/timeout.js +0 -109
  152. package/lib/mochaFactory.js +0 -113
  153. package/lib/plugin/debugErrors.js +0 -67
  154. package/lib/scenario.js +0 -224
  155. package/lib/ui.js +0 -236
package/lib/heal.js CHANGED
@@ -1,122 +1,125 @@
1
- const debug = require('debug')('codeceptjs:heal');
2
- const colors = require('chalk');
3
- const Container = require('./container');
4
- const recorder = require('./recorder');
5
- const output = require('./output');
6
- const event = require('./event');
1
+ const debug = require('debug')('codeceptjs:heal')
2
+ const colors = require('chalk')
3
+ const Container = require('./container')
4
+ const recorder = require('./recorder')
5
+ const output = require('./output')
6
+ const event = require('./event')
7
7
 
8
8
  /**
9
9
  * @class
10
10
  */
11
11
  class Heal {
12
12
  constructor() {
13
- this.recipes = {};
14
- this.fixes = [];
15
- this.prepareFns = [];
16
- this.contextName = null;
17
- this.numHealed = 0;
13
+ this.recipes = {}
14
+ this.fixes = []
15
+ this.prepareFns = []
16
+ this.contextName = null
17
+ this.numHealed = 0
18
18
  }
19
19
 
20
20
  clear() {
21
- this.recipes = {};
22
- this.fixes = [];
23
- this.prepareFns = [];
24
- this.contextName = null;
25
- this.numHealed = 0;
21
+ this.recipes = {}
22
+ this.fixes = []
23
+ this.prepareFns = []
24
+ this.contextName = null
25
+ this.numHealed = 0
26
26
  }
27
27
 
28
28
  addRecipe(name, opts = {}) {
29
- if (!opts.priority) opts.priority = 0;
29
+ if (!opts.priority) opts.priority = 0
30
30
 
31
- if (!opts.fn) throw new Error(`Recipe ${name} should have a function 'fn' to execute`);
31
+ if (!opts.fn) throw new Error(`Recipe ${name} should have a function 'fn' to execute`)
32
32
 
33
- this.recipes[name] = opts;
33
+ this.recipes[name] = opts
34
34
  }
35
35
 
36
36
  connectToEvents() {
37
- event.dispatcher.on(event.suite.before, (suite) => {
38
- this.contextName = suite.title;
39
- });
37
+ event.dispatcher.on(event.suite.before, suite => {
38
+ this.contextName = suite.title
39
+ })
40
40
 
41
- event.dispatcher.on(event.test.started, (test) => {
42
- this.contextName = test.fullTitle();
43
- });
41
+ event.dispatcher.on(event.test.started, test => {
42
+ this.contextName = test.fullTitle()
43
+ })
44
44
 
45
45
  event.dispatcher.on(event.test.finished, () => {
46
- this.contextName = null;
47
- });
46
+ this.contextName = null
47
+ })
48
48
  }
49
49
 
50
50
  hasCorrespondingRecipes(step) {
51
- return matchRecipes(this.recipes, this.contextName)
52
- .filter(r => !r.steps || r.steps.includes(step.name))
53
- .length > 0;
51
+ return matchRecipes(this.recipes, this.contextName).filter(r => !r.steps || r.steps.includes(step.name)).length > 0
54
52
  }
55
53
 
56
54
  async getCodeSuggestions(context) {
57
- const suggestions = [];
58
- const recipes = matchRecipes(this.recipes, this.contextName);
55
+ const suggestions = []
56
+ const recipes = matchRecipes(this.recipes, this.contextName)
59
57
 
60
- debug('Recipes', recipes);
58
+ debug('Recipes', recipes)
61
59
 
62
- const currentOutputLevel = output.level();
63
- output.level(0);
60
+ const currentOutputLevel = output.level()
61
+ output.level(0)
64
62
 
65
- for (const [property, prepareFn] of Object.entries(recipes.map(r => r.prepare).filter(p => !!p).reduce((acc, obj) => ({ ...acc, ...obj }), {}))) {
66
- if (!prepareFn) continue;
63
+ for (const [property, prepareFn] of Object.entries(
64
+ recipes
65
+ .map(r => r.prepare)
66
+ .filter(p => !!p)
67
+ .reduce((acc, obj) => ({ ...acc, ...obj }), {}),
68
+ )) {
69
+ if (!prepareFn) continue
67
70
 
68
- if (context[property]) continue;
69
- context[property] = await prepareFn(Container.support());
71
+ if (context[property]) continue
72
+ context[property] = await prepareFn(Container.support())
70
73
  }
71
74
 
72
- output.level(currentOutputLevel);
75
+ output.level(currentOutputLevel)
73
76
 
74
77
  for (const recipe of recipes) {
75
- let snippets = await recipe.fn(context);
76
- if (!Array.isArray(snippets)) snippets = [snippets];
78
+ let snippets = await recipe.fn(context)
79
+ if (!Array.isArray(snippets)) snippets = [snippets]
77
80
 
78
81
  suggestions.push({
79
82
  name: recipe.name,
80
83
  snippets,
81
- });
84
+ })
82
85
  }
83
86
 
84
- return suggestions.filter(s => !isBlank(s.snippets));
87
+ return suggestions.filter(s => !isBlank(s.snippets))
85
88
  }
86
89
 
87
90
  async healStep(failedStep, error, failureContext = {}) {
88
- output.debug(`Trying to heal ${failedStep.toCode()} step`);
91
+ output.debug(`Trying to heal ${failedStep.toCode()} step`)
89
92
 
90
93
  Object.assign(failureContext, {
91
94
  error,
92
95
  step: failedStep,
93
96
  prevSteps: failureContext?.test?.steps?.slice(0, -1) || [],
94
- });
97
+ })
95
98
 
96
- const suggestions = await this.getCodeSuggestions(failureContext);
99
+ const suggestions = await this.getCodeSuggestions(failureContext)
97
100
 
98
101
  if (suggestions.length === 0) {
99
- debug('No healing suggestions found');
100
- throw error;
102
+ debug('No healing suggestions found')
103
+ throw error
101
104
  }
102
105
 
103
- output.debug(`Received ${suggestions.length} suggestion${suggestions.length === 1 ? '' : 's'}`);
106
+ output.debug(`Received ${suggestions.length} suggestion${suggestions.length === 1 ? '' : 's'}`)
104
107
 
105
- debug(suggestions);
108
+ debug(suggestions)
106
109
 
107
110
  for (const suggestion of suggestions) {
108
111
  for (const codeSnippet of suggestion.snippets) {
109
112
  try {
110
- debug('Executing', codeSnippet);
111
- recorder.catch((e) => {
112
- debug(e);
113
- });
113
+ debug('Executing', codeSnippet)
114
+ recorder.catch(e => {
115
+ debug(e)
116
+ })
114
117
 
115
118
  if (typeof codeSnippet === 'string') {
116
- const I = Container.support('I'); // eslint-disable-line
117
- await eval(codeSnippet); // eslint-disable-line
119
+ const I = Container.support('I')
120
+ await eval(codeSnippet)
118
121
  } else if (typeof codeSnippet === 'function') {
119
- await codeSnippet(Container.support());
122
+ await codeSnippet(Container.support())
120
123
  }
121
124
 
122
125
  this.fixes.push({
@@ -124,49 +127,54 @@ class Heal {
124
127
  test: failureContext?.test,
125
128
  step: failedStep,
126
129
  snippet: codeSnippet,
127
- });
130
+ })
131
+
132
+ if (failureContext?.test) {
133
+ const test = failureContext.test
134
+ let note = `This test was healed by '${suggestion.name}'`
135
+ note += `\n\nReplace the failed code:\n\n`
136
+ note += colors.red(`- ${failedStep.toCode()}\n`)
137
+ note += colors.green(`+ ${codeSnippet}\n`)
138
+ test.addNote('heal', note)
139
+ test.meta.healed = true
140
+ }
128
141
 
129
- recorder.add('healed', () => output.print(colors.bold.green(` Code healed successfully by ${suggestion.name}`), colors.gray('(no errors thrown)')));
130
- this.numHealed++;
142
+ recorder.add('healed', () => output.print(colors.bold.green(` Code healed successfully by ${suggestion.name}`), colors.gray('(no errors thrown)')))
143
+ this.numHealed++
131
144
  // recorder.session.restore();
132
- return;
145
+ return
133
146
  } catch (err) {
134
- debug('Failed to execute code', err);
135
- recorder.ignoreErr(err); // healing did not help
136
- recorder.catchWithoutStop(err);
137
- await recorder.promise(); // wait for all promises to resolve
147
+ debug('Failed to execute code', err)
148
+ recorder.ignoreErr(err) // healing did not help
149
+ recorder.catchWithoutStop(err)
150
+ await recorder.promise() // wait for all promises to resolve
138
151
  }
139
152
  }
140
153
  }
141
- output.debug(`Couldn't heal the code for ${failedStep.toCode()}`);
142
- recorder.throw(error);
154
+ output.debug(`Couldn't heal the code for ${failedStep.toCode()}`)
155
+ recorder.throw(error)
143
156
  }
144
157
 
145
158
  static setDefaultHealers() {
146
- require('./template/heal');
159
+ require('./template/heal')
147
160
  }
148
161
  }
149
162
 
150
- const heal = new Heal();
163
+ const heal = new Heal()
151
164
 
152
- module.exports = heal;
165
+ module.exports = heal
153
166
 
154
167
  function matchRecipes(recipes, contextName) {
155
168
  return Object.entries(recipes)
156
169
  .filter(([, recipe]) => !contextName || !recipe.grep || new RegExp(recipe.grep).test(contextName))
157
170
  .sort(([, a], [, b]) => a.priority - b.priority)
158
171
  .map(([name, recipe]) => {
159
- recipe.name = name;
160
- return recipe;
172
+ recipe.name = name
173
+ return recipe
161
174
  })
162
- .filter(r => !!r.fn);
175
+ .filter(r => !!r.fn)
163
176
  }
164
177
 
165
178
  function isBlank(value) {
166
- return (
167
- value == null
168
- || (Array.isArray(value) && value.length === 0)
169
- || (typeof value === 'object' && Object.keys(value).length === 0)
170
- || (typeof value === 'string' && value.trim() === '')
171
- );
179
+ return value == null || (Array.isArray(value) && value.length === 0) || (typeof value === 'object' && Object.keys(value).length === 0) || (typeof value === 'string' && value.trim() === '')
172
180
  }
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
  }
@@ -51,7 +51,7 @@ const REST = require('./REST')
51
51
  *
52
52
  * module.exports = new Factory()
53
53
  * // no need to set id, it will be set by REST API
54
- * .attr('author', () => faker.name.findName())
54
+ * .attr('author', () => faker.person.findName())
55
55
  * .attr('title', () => faker.lorem.sentence())
56
56
  * .attr('body', () => faker.lorem.paragraph());
57
57
  * ```
@@ -217,7 +217,7 @@ class ApiDataFactory extends Helper {
217
217
  }
218
218
 
219
219
  this.created = {}
220
- Object.keys(this.factories).forEach((f) => (this.created[f] = []))
220
+ Object.keys(this.factories).forEach(f => (this.created[f] = []))
221
221
  }
222
222
 
223
223
  static _checkRequirements() {
@@ -357,7 +357,7 @@ Current file error: ${err.message}`)
357
357
 
358
358
  request.baseURL = this.config.endpoint
359
359
 
360
- return this.restHelper._executeRequest(request).then((resp) => {
360
+ return this.restHelper._executeRequest(request).then(resp => {
361
361
  const id = this._fetchId(resp.data, factory)
362
362
  this.created[factory].push(id)
363
363
  this.debugSection('Created', `Id: ${id}`)
@@ -391,10 +391,7 @@ Current file error: ${err.message}`)
391
391
  request.baseURL = this.config.endpoint
392
392
 
393
393
  if (request.url.match(/^undefined/)) {
394
- return this.debugSection(
395
- 'Please configure the delete request in your ApiDataFactory helper',
396
- "delete: () => ({ method: 'DELETE', url: '/api/users' })",
397
- )
394
+ return this.debugSection('Please configure the delete request in your ApiDataFactory helper', "delete: () => ({ method: 'DELETE', url: '/api/users' })")
398
395
  }
399
396
 
400
397
  return this.restHelper._executeRequest(request).then(() => {
@@ -44,7 +44,7 @@ const vendorPrefix = {
44
44
  *
45
45
  * This helper should be configured in codecept.conf.ts or codecept.conf.js
46
46
  *
47
- * * `appiumV2`: set this to true if you want to run tests with AppiumV2. See more how to setup [here](https://codecept.io/mobile/#setting-up)
47
+ * * `appiumV2`: by default is true, set this to false if you want to run tests with AppiumV1. See more how to setup [here](https://codecept.io/mobile/#setting-up)
48
48
  * * `app`: Application path. Local path or remote URL to an .ipa or .apk file, or a .zip containing one of these. Alias to desiredCapabilities.appPackage
49
49
  * * `host`: (default: 'localhost') Appium host
50
50
  * * `port`: (default: '4723') Appium port
@@ -124,7 +124,7 @@ const vendorPrefix = {
124
124
  * {
125
125
  * helpers: {
126
126
  * Appium: {
127
- * appiumV2: true,
127
+ * appiumV2: true, // By default is true, set to false if you want to run against Appium v1
128
128
  * host: "hub-cloud.browserstack.com",
129
129
  * port: 4444,
130
130
  * user: process.env.BROWSERSTACK_USER,
@@ -178,16 +178,12 @@ class Appium extends Webdriver {
178
178
  super(config)
179
179
 
180
180
  this.isRunning = false
181
- if (config.appiumV2 === true) {
182
- this.appiumV2 = true
183
- }
181
+ this.appiumV2 = config.appiumV2 || true
184
182
  this.axios = axios.create()
185
183
 
186
184
  webdriverio = require('webdriverio')
187
185
  if (!config.appiumV2) {
188
- console.log(
189
- 'The Appium core team does not maintain Appium 1.x anymore since the 1st of January 2022. Please migrating to Appium 2.x by adding appiumV2: true to your config.',
190
- )
186
+ console.log('The Appium core team does not maintain Appium 1.x anymore since the 1st of January 2022. Appium 2.x is used by default.')
191
187
  console.log('More info: https://bit.ly/appium-v2-migration')
192
188
  console.log('This Appium 1.x support will be removed in next major release.')
193
189
  }
@@ -234,20 +230,14 @@ class Appium extends Webdriver {
234
230
 
235
231
  config.baseUrl = config.url || config.baseUrl
236
232
  if (config.desiredCapabilities && Object.keys(config.desiredCapabilities).length) {
237
- config.capabilities =
238
- this.appiumV2 === true ? this._convertAppiumV2Caps(config.desiredCapabilities) : config.desiredCapabilities
233
+ config.capabilities = this.appiumV2 === true ? this._convertAppiumV2Caps(config.desiredCapabilities) : config.desiredCapabilities
239
234
  }
240
235
 
241
236
  if (this.appiumV2) {
242
- config.capabilities[`${vendorPrefix.appium}:deviceName`] =
243
- config[`${vendorPrefix.appium}:device`] || config.capabilities[`${vendorPrefix.appium}:deviceName`]
244
- config.capabilities[`${vendorPrefix.appium}:browserName`] =
245
- config[`${vendorPrefix.appium}:browser`] || config.capabilities[`${vendorPrefix.appium}:browserName`]
246
- config.capabilities[`${vendorPrefix.appium}:app`] =
247
- config[`${vendorPrefix.appium}:app`] || config.capabilities[`${vendorPrefix.appium}:app`]
248
- config.capabilities[`${vendorPrefix.appium}:tunnelIdentifier`] =
249
- config[`${vendorPrefix.appium}:tunnelIdentifier`] ||
250
- config.capabilities[`${vendorPrefix.appium}:tunnelIdentifier`] // Adding the code to connect to sauce labs via sauce tunnel
237
+ config.capabilities[`${vendorPrefix.appium}:deviceName`] = config[`${vendorPrefix.appium}:device`] || config.capabilities[`${vendorPrefix.appium}:deviceName`]
238
+ config.capabilities[`${vendorPrefix.appium}:browserName`] = config[`${vendorPrefix.appium}:browser`] || config.capabilities[`${vendorPrefix.appium}:browserName`]
239
+ config.capabilities[`${vendorPrefix.appium}:app`] = config[`${vendorPrefix.appium}:app`] || config.capabilities[`${vendorPrefix.appium}:app`]
240
+ config.capabilities[`${vendorPrefix.appium}:tunnelIdentifier`] = config[`${vendorPrefix.appium}:tunnelIdentifier`] || config.capabilities[`${vendorPrefix.appium}:tunnelIdentifier`] // Adding the code to connect to sauce labs via sauce tunnel
251
241
  } else {
252
242
  config.capabilities.deviceName = config.device || config.capabilities.deviceName
253
243
  config.capabilities.browserName = config.browser || config.capabilities.browserName
@@ -393,8 +383,10 @@ class Appium extends Webdriver {
393
383
 
394
384
  _buildAppiumEndpoint() {
395
385
  const { protocol, port, hostname, path } = this.browser.options
386
+ // Ensure path does NOT end with a slash to prevent double slashes
387
+ const normalizedPath = path.replace(/\/$/, '')
396
388
  // Build path to Appium REST API endpoint
397
- return `${protocol}://${hostname}:${port}${path}`
389
+ return `${protocol}://${hostname}:${port}${normalizedPath}/session/${this.browser.sessionId}`
398
390
  }
399
391
 
400
392
  /**
@@ -491,17 +483,15 @@ class Appium extends Webdriver {
491
483
  * });
492
484
  * ```
493
485
  *
494
- * @param {*} fn
495
486
  */
496
- /* eslint-disable */
497
- async runInWeb(fn) {
487
+
488
+ async runInWeb() {
498
489
  if (!this.isWeb) return
499
490
  recorder.session.start('Web-only actions')
500
491
 
501
492
  recorder.add('restore from Web session', () => recorder.session.restore(), true)
502
493
  return recorder.promise()
503
494
  }
504
- /* eslint-enable */
505
495
 
506
496
  _runWithCaps(caps, fn) {
507
497
  if (typeof caps === 'object') {
@@ -612,7 +602,7 @@ class Appium extends Webdriver {
612
602
 
613
603
  return this.axios({
614
604
  method: 'post',
615
- url: `${this._buildAppiumEndpoint()}/session/${this.browser.sessionId}/appium/device/remove_app`,
605
+ url: `${this._buildAppiumEndpoint()}/appium/device/remove_app`,
616
606
  data: { appId, bundleId },
617
607
  })
618
608
  }
@@ -629,7 +619,7 @@ class Appium extends Webdriver {
629
619
  onlyForApps.call(this)
630
620
  return this.axios({
631
621
  method: 'post',
632
- url: `${this._buildAppiumEndpoint()}/session/${this.browser.sessionId}/appium/app/reset`,
622
+ url: `${this._buildAppiumEndpoint()}/appium/app/reset`,
633
623
  })
634
624
  }
635
625
 
@@ -703,7 +693,7 @@ class Appium extends Webdriver {
703
693
 
704
694
  const res = await this.axios({
705
695
  method: 'get',
706
- url: `${this._buildAppiumEndpoint()}/session/${this.browser.sessionId}/orientation`,
696
+ url: `${this._buildAppiumEndpoint()}/orientation`,
707
697
  })
708
698
 
709
699
  const currentOrientation = res.data.value
@@ -727,7 +717,7 @@ class Appium extends Webdriver {
727
717
 
728
718
  return this.axios({
729
719
  method: 'post',
730
- url: `${this._buildAppiumEndpoint()}/session/${this.browser.sessionId}/orientation`,
720
+ url: `${this._buildAppiumEndpoint()}/orientation`,
731
721
  data: { orientation },
732
722
  })
733
723
  }
@@ -966,21 +956,19 @@ class Appium extends Webdriver {
966
956
  * ```js
967
957
  * // taps outside to hide keyboard per default
968
958
  * I.hideDeviceKeyboard();
969
- * I.hideDeviceKeyboard('tapOutside');
970
- *
971
- * // or by pressing key
972
- * I.hideDeviceKeyboard('pressKey', 'Done');
973
959
  * ```
974
960
  *
975
961
  * Appium: support Android and iOS
976
962
  *
977
- * @param {'tapOutside' | 'pressKey'} [strategy] Desired strategy to close keyboard (‘tapOutside’ or ‘pressKey’)
978
- * @param {string} [key] Optional key
979
963
  */
980
- async hideDeviceKeyboard(strategy, key) {
964
+ async hideDeviceKeyboard() {
981
965
  onlyForApps.call(this)
982
- strategy = strategy || 'tapOutside'
983
- 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
+ })
984
972
  }
985
973
 
986
974
  /**
@@ -1056,7 +1044,13 @@ class Appium extends Webdriver {
1056
1044
  * @param {*} locator
1057
1045
  */
1058
1046
  async tap(locator) {
1059
- 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
+ })
1060
1054
  }
1061
1055
 
1062
1056
  /**
@@ -1077,7 +1071,7 @@ class Appium extends Webdriver {
1077
1071
  *
1078
1072
  * Appium: support Android and iOS
1079
1073
  */
1080
- /* eslint-disable */
1074
+
1081
1075
  async swipe(locator, xoffset, yoffset, speed = 1000) {
1082
1076
  onlyForApps.call(this)
1083
1077
  const res = await this.browser.$(parseLocator.call(this, locator))
@@ -1087,7 +1081,6 @@ class Appium extends Webdriver {
1087
1081
  y: (await res.getLocation()).y + yoffset,
1088
1082
  })
1089
1083
  }
1090
- /* eslint-enable */
1091
1084
 
1092
1085
  /**
1093
1086
  * Perform a swipe on the screen.
@@ -1307,14 +1300,14 @@ class Appium extends Webdriver {
1307
1300
  }
1308
1301
  return browser
1309
1302
  .$$(parseLocator.call(this, searchableLocator))
1310
- .then((els) => els.length && els[0].isDisplayed())
1311
- .then((res) => {
1303
+ .then(els => els.length && els[0].isDisplayed())
1304
+ .then(res => {
1312
1305
  if (res) {
1313
1306
  return true
1314
1307
  }
1315
1308
  return this[direction](scrollLocator, offset, speed)
1316
1309
  .getSource()
1317
- .then((source) => {
1310
+ .then(source => {
1318
1311
  if (source === currentSource) {
1319
1312
  err = true
1320
1313
  } else {
@@ -1327,12 +1320,9 @@ class Appium extends Webdriver {
1327
1320
  timeout * 1000,
1328
1321
  errorMsg,
1329
1322
  )
1330
- .catch((e) => {
1323
+ .catch(e => {
1331
1324
  if (e.message.indexOf('timeout') && e.type !== 'NoSuchElement') {
1332
- throw new AssertionFailedError(
1333
- { customMessage: `Scroll to the end and element ${searchableLocator} was not found` },
1334
- '',
1335
- )
1325
+ throw new AssertionFailedError({ customMessage: `Scroll to the end and element ${searchableLocator} was not found` }, '')
1336
1326
  } else {
1337
1327
  throw e
1338
1328
  }
@@ -1389,8 +1379,8 @@ class Appium extends Webdriver {
1389
1379
  */
1390
1380
  async pullFile(path, dest) {
1391
1381
  onlyForApps.call(this)
1392
- return this.browser.pullFile(path).then((res) =>
1393
- fs.writeFile(dest, Buffer.from(res, 'base64'), (err) => {
1382
+ return this.browser.pullFile(path).then(res =>
1383
+ fs.writeFile(dest, Buffer.from(res, 'base64'), err => {
1394
1384
  if (err) {
1395
1385
  return false
1396
1386
  }
@@ -1507,7 +1497,14 @@ class Appium extends Webdriver {
1507
1497
  */
1508
1498
  async click(locator, context) {
1509
1499
  if (this.isWeb) return super.click(locator, context)
1510
- 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
+ })
1511
1508
  }
1512
1509
 
1513
1510
  /**
@@ -1762,12 +1759,8 @@ function parseLocator(locator) {
1762
1759
  }
1763
1760
 
1764
1761
  locator = new Locator(locator, 'xpath')
1765
- if (locator.type === 'css' && !this.isWeb)
1766
- throw new Error(
1767
- 'Unable to use css locators in apps. Locator strategies for this request: xpath, id, class name or accessibility id',
1768
- )
1769
- if (locator.type === 'name' && !this.isWeb)
1770
- throw new Error("Can't locate element by name in Native context. Use either ID, class name or accessibility id")
1762
+ if (locator.type === 'css' && !this.isWeb) throw new Error('Unable to use css locators in apps. Locator strategies for this request: xpath, id, class name or accessibility id')
1763
+ if (locator.type === 'name' && !this.isWeb) throw new Error("Can't locate element by name in Native context. Use either ID, class name or accessibility id")
1771
1764
  if (locator.type === 'id' && !this.isWeb && this.platform === 'android') return `//*[@resource-id='${locator.value}']`
1772
1765
  return locator.simplify()
1773
1766
  }
@@ -105,7 +105,7 @@ class FileSystem extends Helper {
105
105
  */
106
106
  seeFileNameMatching(text) {
107
107
  assert.ok(
108
- this.grabFileNames().some((file) => file.includes(text)),
108
+ this.grabFileNames().some(file => file.includes(text)),
109
109
  `File name which contains ${text} not found in ${this.dir}`,
110
110
  )
111
111
  }
@@ -175,7 +175,7 @@ class FileSystem extends Helper {
175
175
  * ```
176
176
  */
177
177
  grabFileNames() {
178
- return fs.readdirSync(this.dir).filter((item) => !fs.lstatSync(path.join(this.dir, item)).isDirectory())
178
+ return fs.readdirSync(this.dir).filter(item => !fs.lstatSync(path.join(this.dir, item)).isDirectory())
179
179
  }
180
180
  }
181
181
 
@@ -216,7 +216,7 @@ function isFileExists(file, timeout) {
216
216
  }
217
217
  })
218
218
 
219
- fs.access(file, fs.constants.R_OK, (err) => {
219
+ fs.access(file, fs.constants.R_OK, err => {
220
220
  if (!err) {
221
221
  clearTimeout(timer)
222
222
  watcher.close()
@@ -55,7 +55,7 @@ const GraphQL = require('./GraphQL')
55
55
  * input: { ...buildObj },
56
56
  * }))
57
57
  * // 'attr'-id can be left out depending on the GraphQl resolvers
58
- * .attr('name', () => faker.name.findName())
58
+ * .attr('name', () => faker.person.findName())
59
59
  * .attr('email', () => faker.interact.email())
60
60
  * ```
61
61
  * For more options see [rosie documentation](https://github.com/rosiejs/rosie).
@@ -170,7 +170,7 @@ class GraphQLDataFactory extends Helper {
170
170
  this.factories = this.config.factories
171
171
 
172
172
  this.created = {}
173
- Object.keys(this.factories).forEach((f) => (this.created[f] = []))
173
+ Object.keys(this.factories).forEach(f => (this.created[f] = []))
174
174
  }
175
175
 
176
176
  static _checkRequirements() {
@@ -278,7 +278,7 @@ class GraphQLDataFactory extends Helper {
278
278
  */
279
279
  _requestCreate(operation, variables) {
280
280
  const { query } = this.factories[operation]
281
- return this.graphqlHelper.sendMutation(query, variables).then((response) => {
281
+ return this.graphqlHelper.sendMutation(query, variables).then(response => {
282
282
  const data = response.data.data[operation]
283
283
  this.created[operation].push(data)
284
284
  this.debugSection('Created', `record: ${data}`)
@@ -297,7 +297,7 @@ class GraphQLDataFactory extends Helper {
297
297
  const deleteOperation = this.factories[operation].revert(data)
298
298
  const { query, variables } = deleteOperation
299
299
 
300
- return this.graphqlHelper.sendMutation(query, variables).then((response) => {
300
+ return this.graphqlHelper.sendMutation(query, variables).then(response => {
301
301
  const idx = this.created[operation].indexOf(data)
302
302
  this.debugSection('Deleted', `record: ${response.data.data}`)
303
303
  this.created[operation].splice(idx, 1)