codeceptjs 3.4.0 → 3.5.0

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 (71) hide show
  1. package/CHANGELOG.md +70 -0
  2. package/README.md +9 -7
  3. package/bin/codecept.js +1 -1
  4. package/docs/ai.md +246 -0
  5. package/docs/build/Appium.js +47 -7
  6. package/docs/build/JSONResponse.js +4 -4
  7. package/docs/build/Nightmare.js +3 -1
  8. package/docs/build/OpenAI.js +122 -0
  9. package/docs/build/Playwright.js +193 -45
  10. package/docs/build/Protractor.js +3 -1
  11. package/docs/build/Puppeteer.js +45 -12
  12. package/docs/build/REST.js +15 -5
  13. package/docs/build/TestCafe.js +3 -1
  14. package/docs/build/WebDriver.js +30 -5
  15. package/docs/changelog.md +70 -0
  16. package/docs/helpers/Appium.md +152 -147
  17. package/docs/helpers/JSONResponse.md +4 -4
  18. package/docs/helpers/Nightmare.md +2 -0
  19. package/docs/helpers/OpenAI.md +70 -0
  20. package/docs/helpers/Playwright.md +194 -152
  21. package/docs/helpers/Puppeteer.md +6 -0
  22. package/docs/helpers/REST.md +6 -5
  23. package/docs/helpers/TestCafe.md +2 -0
  24. package/docs/helpers/WebDriver.md +10 -4
  25. package/docs/mobile.md +49 -2
  26. package/docs/parallel.md +56 -0
  27. package/docs/plugins.md +87 -33
  28. package/docs/secrets.md +6 -0
  29. package/docs/tutorial.md +5 -5
  30. package/docs/webapi/appendField.mustache +2 -0
  31. package/docs/webapi/type.mustache +3 -0
  32. package/lib/ai.js +171 -0
  33. package/lib/cli.js +1 -1
  34. package/lib/codecept.js +4 -0
  35. package/lib/command/dryRun.js +9 -1
  36. package/lib/command/generate.js +46 -3
  37. package/lib/command/init.js +13 -1
  38. package/lib/command/interactive.js +15 -1
  39. package/lib/command/run-workers.js +2 -1
  40. package/lib/container.js +13 -3
  41. package/lib/helper/Appium.js +45 -7
  42. package/lib/helper/JSONResponse.js +4 -4
  43. package/lib/helper/Nightmare.js +1 -1
  44. package/lib/helper/OpenAI.js +122 -0
  45. package/lib/helper/Playwright.js +190 -38
  46. package/lib/helper/Protractor.js +1 -1
  47. package/lib/helper/Puppeteer.js +40 -12
  48. package/lib/helper/REST.js +15 -5
  49. package/lib/helper/TestCafe.js +1 -1
  50. package/lib/helper/WebDriver.js +25 -5
  51. package/lib/helper/scripts/highlightElement.js +20 -0
  52. package/lib/html.js +258 -0
  53. package/lib/listener/retry.js +2 -1
  54. package/lib/pause.js +73 -17
  55. package/lib/plugin/debugErrors.js +67 -0
  56. package/lib/plugin/fakerTransform.js +4 -6
  57. package/lib/plugin/heal.js +179 -0
  58. package/lib/plugin/screenshotOnFail.js +11 -2
  59. package/lib/plugin/wdio.js +4 -12
  60. package/lib/recorder.js +4 -4
  61. package/lib/scenario.js +6 -4
  62. package/lib/secret.js +5 -4
  63. package/lib/step.js +6 -1
  64. package/lib/ui.js +4 -3
  65. package/lib/utils.js +4 -0
  66. package/lib/workers.js +57 -9
  67. package/package.json +26 -14
  68. package/translations/ja-JP.js +9 -9
  69. package/typings/index.d.ts +43 -9
  70. package/typings/promiseBasedTypes.d.ts +124 -24
  71. package/typings/types.d.ts +138 -30
package/lib/ai.js ADDED
@@ -0,0 +1,171 @@
1
+ const { Configuration, OpenAIApi } = require('openai');
2
+ const debug = require('debug')('codeceptjs:ai');
3
+ const config = require('./config');
4
+ const output = require('./output');
5
+ const { removeNonInteractiveElements, minifyHtml, splitByChunks } = require('./html');
6
+
7
+ const defaultConfig = {
8
+ model: 'gpt-3.5-turbo-16k',
9
+ temperature: 0.1,
10
+ };
11
+
12
+ const htmlConfig = {
13
+ maxLength: 50000,
14
+ simplify: true,
15
+ minify: true,
16
+ html: {},
17
+ };
18
+
19
+ class AiAssistant {
20
+ constructor() {
21
+ this.config = config.get('ai', defaultConfig);
22
+ this.htmlConfig = Object.assign(htmlConfig, this.config.html);
23
+ delete this.config.html;
24
+ this.html = null;
25
+ this.response = null;
26
+
27
+ this.isEnabled = !!process.env.OPENAI_API_KEY;
28
+
29
+ if (!this.isEnabled) return;
30
+
31
+ const configuration = new Configuration({
32
+ apiKey: process.env.OPENAI_API_KEY,
33
+ });
34
+
35
+ this.openai = new OpenAIApi(configuration);
36
+ }
37
+
38
+ setHtmlContext(html) {
39
+ let processedHTML = html;
40
+
41
+ if (this.htmlConfig.simplify) {
42
+ processedHTML = removeNonInteractiveElements(processedHTML, this.htmlConfig);
43
+ }
44
+ if (this.htmlConfig.minify) processedHTML = minifyHtml(processedHTML);
45
+ if (this.htmlConfig.maxLength) processedHTML = splitByChunks(processedHTML, this.htmlConfig.maxLength)[0];
46
+
47
+ debug(processedHTML);
48
+
49
+ this.html = processedHTML;
50
+ }
51
+
52
+ getResponse() {
53
+ return this.response || '';
54
+ }
55
+
56
+ mockResponse(response) {
57
+ this.mockedResponse = response;
58
+ }
59
+
60
+ async createCompletion(messages) {
61
+ if (!this.openai) return;
62
+
63
+ debug(messages);
64
+
65
+ if (this.mockedResponse) return this.mockedResponse;
66
+
67
+ this.response = null;
68
+
69
+ try {
70
+ const completion = await this.openai.createChatCompletion({
71
+ ...this.config,
72
+ messages,
73
+ });
74
+
75
+ this.response = completion?.data?.choices[0]?.message?.content;
76
+
77
+ debug(this.response);
78
+
79
+ return this.response;
80
+ } catch (err) {
81
+ debug(err.response);
82
+ output.print('');
83
+ output.error(`OpenAI error: ${err.message}`);
84
+ output.error(err?.response?.data?.error?.code);
85
+ output.error(err?.response?.data?.error?.message);
86
+ return '';
87
+ }
88
+ }
89
+
90
+ async healFailedStep(step, err, test) {
91
+ if (!this.isEnabled) return [];
92
+ if (!this.html) throw new Error('No HTML context provided');
93
+
94
+ const messages = [
95
+ { role: 'user', content: 'As a test automation engineer I am testing web application using CodeceptJS.' },
96
+ { role: 'user', content: `I want to heal a test that fails. Here is the list of executed steps: ${test.steps.join(', ')}` },
97
+ { role: 'user', content: `Propose how to adjust ${step.toCode()} step to fix the test.` },
98
+ { role: 'user', content: 'Use locators in order of preference: semantic locator by text, CSS, XPath. Use codeblocks marked with ```.' },
99
+ { role: 'user', content: `Here is the error message: ${err.message}` },
100
+ { role: 'user', content: `Here is HTML code of a page where the failure has happened: \n\n${this.html}` },
101
+ ];
102
+
103
+ const response = await this.createCompletion(messages);
104
+ if (!response) return [];
105
+
106
+ return parseCodeBlocks(response);
107
+ }
108
+
109
+ async writeSteps(input) {
110
+ if (!this.isEnabled) return;
111
+ if (!this.html) throw new Error('No HTML context provided');
112
+
113
+ const snippets = [];
114
+
115
+ const messages = [
116
+ {
117
+ role: 'user',
118
+ content: `I am test engineer writing test in CodeceptJS
119
+ I have opened web page and I want to use CodeceptJS to ${input} on this page
120
+ Provide me valid CodeceptJS code to accomplish it
121
+ Use only locators from this HTML: \n\n${this.html}`,
122
+ },
123
+ { role: 'user', content: 'Propose only CodeceptJS steps code. Do not include Scenario or Feature into response' },
124
+
125
+ // old prompt
126
+ // { role: 'user', content: 'I want to click button Submit using CodeceptJS on this HTML page: <html><body><button>Submit</button></body></html>' },
127
+ // { role: 'assistant', content: '```js\nI.click("Submit");\n```' },
128
+ // { role: 'user', content: 'I want to click button Submit using CodeceptJS on this HTML page: <html><body><button>Login</button></body></html>' },
129
+ // { role: 'assistant', content: 'No suggestions' },
130
+ // { role: 'user', content: `Now I want to ${input} on this HTML page using CodeceptJS code` },
131
+ // { role: 'user', content: `Provide me with CodeceptJS code to achieve this on THIS page.` },
132
+ ];
133
+ const response = await this.createCompletion(messages);
134
+ if (!response) return;
135
+ snippets.push(...parseCodeBlocks(response));
136
+
137
+ debug(snippets[0]);
138
+
139
+ return snippets[0];
140
+ }
141
+ }
142
+
143
+ function parseCodeBlocks(response) {
144
+ // Regular expression pattern to match code snippets
145
+ const codeSnippetPattern = /```(?:javascript|js|typescript|ts)?\n([\s\S]+?)\n```/g;
146
+
147
+ // Array to store extracted code snippets
148
+ const codeSnippets = [];
149
+
150
+ response = response.split('\n').map(line => line.trim()).join('\n');
151
+
152
+ // Iterate over matches and extract code snippets
153
+ let match;
154
+ while ((match = codeSnippetPattern.exec(response)) !== null) {
155
+ codeSnippets.push(match[1]);
156
+ }
157
+
158
+ // Remove "Scenario", "Feature", and "require()" lines
159
+ const modifiedSnippets = codeSnippets.map(snippet => {
160
+ const lines = snippet.split('\n');
161
+
162
+ const filteredLines = lines.filter(line => !line.includes('I.amOnPage') && !line.startsWith('Scenario') && !line.startsWith('Feature') && !line.includes('= require('));
163
+
164
+ return filteredLines.join('\n');
165
+ // remove snippets that move from current url
166
+ }); // .filter(snippet => !line.includes('I.amOnPage'));
167
+
168
+ return modifiedSnippets.filter(snippet => !!snippet);
169
+ }
170
+
171
+ module.exports = AiAssistant;
package/lib/cli.js CHANGED
@@ -168,7 +168,7 @@ class Cli extends Base {
168
168
  }
169
169
 
170
170
  // display artifacts in debug mode
171
- if (test.artifacts && Object.keys(test.artifacts).length) {
171
+ if (test?.artifacts && Object.keys(test.artifacts).length) {
172
172
  log += `\n${output.styles.bold('Artifacts:')}`;
173
173
  for (const artifact of Object.keys(test.artifacts)) {
174
174
  log += `\n- ${artifact}: ${test.artifacts[artifact]}`;
package/lib/codecept.js CHANGED
@@ -8,6 +8,7 @@ const Config = require('./config');
8
8
  const event = require('./event');
9
9
  const runHook = require('./hooks');
10
10
  const output = require('./output');
11
+ const { emptyFolder } = require('./utils');
11
12
 
12
13
  /**
13
14
  * CodeceptJS runner
@@ -66,6 +67,8 @@ class Codecept {
66
67
  global.codecept_dir = dir;
67
68
  global.output_dir = fsPath.resolve(dir, this.config.output);
68
69
 
70
+ if (this.config.emptyOutputFolder) emptyFolder(global.output_dir);
71
+
69
72
  if (!this.config.noGlobals) {
70
73
  global.Helper = global.codecept_helper = require('@codeceptjs/helper');
71
74
  global.actor = global.codecept_actor = require('./actor');
@@ -158,6 +161,7 @@ class Codecept {
158
161
 
159
162
  for (pattern of patterns) {
160
163
  glob.sync(pattern, options).forEach((file) => {
164
+ if (file.includes('node_modules')) return;
161
165
  if (!fsPath.isAbsolute(file)) {
162
166
  file = fsPath.join(global.codecept_dir, file);
163
167
  }
@@ -7,6 +7,7 @@ const store = require('../store');
7
7
  const Container = require('../container');
8
8
 
9
9
  module.exports = async function (test, options) {
10
+ if (options.grep) process.env.grep = options.grep.toLowerCase();
10
11
  const configFile = options.config;
11
12
  let codecept;
12
13
 
@@ -57,9 +58,16 @@ function printTests(files) {
57
58
 
58
59
  let numOfTests = 0;
59
60
  let numOfSuites = 0;
61
+ const filteredSuites = [];
60
62
 
61
63
  for (const suite of mocha.suite.suites) {
62
- output.print(`${colors.white.bold(suite.title)} -- ${output.styles.log(suite.file || '')}`);
64
+ if (process.env.grep && suite.title.toLowerCase().includes(process.env.grep)) {
65
+ filteredSuites.push(suite);
66
+ }
67
+ }
68
+ const displayedSuites = process.env.grep ? filteredSuites : mocha.suite.suites;
69
+ for (const suite of displayedSuites) {
70
+ output.print(`${colors.white.bold(suite.title)} -- ${output.styles.log(suite.file || '')} -- ${suite.tests.length} tests`);
63
71
  numOfSuites++;
64
72
 
65
73
  for (const test of suite.tests) {
@@ -24,6 +24,7 @@ Scenario('test something', async ({ {{actor}} }) => {
24
24
  // generates empty test
25
25
  module.exports.test = function (genPath) {
26
26
  const testsPath = getTestRoot(genPath);
27
+ global.codecept_dir = testsPath;
27
28
  const config = getConfig(testsPath);
28
29
  if (!config) return;
29
30
 
@@ -83,6 +84,29 @@ module.exports = {
83
84
  }
84
85
  `;
85
86
 
87
+ const poModuleTemplateTS = `const { I } = inject();
88
+
89
+ export = {
90
+
91
+ // insert your locators and methods here
92
+ }
93
+ `;
94
+
95
+ const poClassTemplate = `const { I } = inject();
96
+
97
+ class {{name}} {
98
+ constructor() {
99
+ //insert your locators
100
+ // this.button = '#button'
101
+ }
102
+ // insert your methods here
103
+ }
104
+
105
+ // For inheritance
106
+ module.exports = new {{name}}();
107
+ export = {{name}};
108
+ `;
109
+
86
110
  module.exports.pageObject = function (genPath, opts) {
87
111
  const testsPath = getTestRoot(genPath);
88
112
  const config = getConfig(testsPath);
@@ -110,7 +134,15 @@ module.exports.pageObject = function (genPath, opts) {
110
134
  name: 'filename',
111
135
  message: 'Where should it be stored',
112
136
  default: answers => `./${kind}s/${answers.name}.${extension}`,
113
- }]).then((result) => {
137
+ },
138
+ {
139
+ type: 'list',
140
+ name: 'objectType',
141
+ message: 'What is your preferred object type',
142
+ choices: ['module', 'class'],
143
+ default: 'module',
144
+ },
145
+ ]).then((result) => {
114
146
  const pageObjectFile = path.join(testsPath, result.filename);
115
147
  const dir = path.dirname(pageObjectFile);
116
148
  if (!fileExists(dir)) fs.mkdirSync(dir);
@@ -124,13 +156,24 @@ module.exports.pageObject = function (genPath, opts) {
124
156
  }
125
157
  actor = `require('${actorPath}')`;
126
158
  }
127
- if (!safeFileWrite(pageObjectFile, pageObjectTemplate.replace('{{actor}}', actor))) return;
159
+
128
160
  const name = lcfirst(result.name) + ucfirst(kind);
161
+ if (result.objectType === 'module' && extension === 'ts') {
162
+ if (!safeFileWrite(pageObjectFile, poModuleTemplateTS.replace('{{actor}}', actor))) return;
163
+ } else if (result.objectType === 'module' && extension === 'js') {
164
+ if (!safeFileWrite(pageObjectFile, pageObjectTemplate.replace('{{actor}}', actor))) return;
165
+ } else if (result.objectType === 'class') {
166
+ const content = poClassTemplate.replace(/{{actor}}/g, actor).replace(/{{name}}/g, name);
167
+ if (!safeFileWrite(pageObjectFile, content)) return;
168
+ }
169
+
129
170
  let data = readConfig(configFile);
130
171
  config.include[name] = result.filename;
172
+
173
+ if (!data) throw Error('Config file is empty');
131
174
  const currentInclude = `${data.match(/include:[\s\S][^\}]*/i)[0]}\n ${name}:${JSON.stringify(config.include[name])}`;
132
175
 
133
- data = data.replace(/include:[\s\S][^\}]*/i, `${currentInclude}`);
176
+ data = data.replace(/include:[\s\S][^\}]*/i, `${currentInclude},`);
134
177
 
135
178
  fs.writeFileSync(configFile, beautify(data), 'utf-8');
136
179
 
@@ -55,6 +55,18 @@ module.exports = function() {
55
55
  }
56
56
  `;
57
57
 
58
+ const defaultActorTs = `// in this file you can append custom step methods to 'I' object
59
+
60
+ export = function() {
61
+ return actor({
62
+
63
+ // Define custom steps here, use 'this' to access default methods of I.
64
+ // It is recommended to place a general 'login' function here.
65
+
66
+ });
67
+ }
68
+ `;
69
+
58
70
  module.exports = function (initPath) {
59
71
  const testsPath = getTestRoot(initPath);
60
72
 
@@ -188,7 +200,7 @@ module.exports = function (initPath) {
188
200
  // create steps file by default
189
201
  // no extra step file for typescript (as it doesn't match TS conventions)
190
202
  const stepFile = `./steps_file.${extension}`;
191
- fs.writeFileSync(path.join(testsPath, stepFile), defaultActor);
203
+ fs.writeFileSync(path.join(testsPath, stepFile), extension === 'ts' ? defaultActorTs : defaultActor);
192
204
  config.include.I = isTypeScript === true ? './steps_file' : stepFile;
193
205
  print(`Steps file created at ${stepFile}`);
194
206
 
@@ -1,8 +1,10 @@
1
1
  const { getConfig, getTestRoot } = require('./utils');
2
2
  const recorder = require('../recorder');
3
3
  const Codecept = require('../codecept');
4
+ const Container = require('../container');
4
5
  const event = require('../event');
5
6
  const output = require('../output');
7
+ const webHelpers = require('../plugin/standardActingHelpers');
6
8
 
7
9
  module.exports = async function (path, options) {
8
10
  // Backward compatibility for --profile
@@ -29,9 +31,21 @@ module.exports = async function (path, options) {
29
31
  });
30
32
  event.emit(event.test.before, {
31
33
  title: '',
34
+ artifacts: {},
32
35
  });
36
+
37
+ const enabledHelpers = Container.helpers();
38
+ for (const helperName of Object.keys(enabledHelpers)) {
39
+ if (webHelpers.includes(helperName)) {
40
+ const I = enabledHelpers[helperName];
41
+ recorder.add(() => I.amOnPage('/'));
42
+ recorder.catchWithoutStop(e => output.print(`Error while loading home page: ${e.message}}`));
43
+ break;
44
+ }
45
+ }
33
46
  require('../pause')();
34
- recorder.add(() => event.emit(event.test.after));
47
+ // recorder.catchWithoutStop((err) => console.log(err.stack));
48
+ recorder.add(() => event.emit(event.test.after, {}));
35
49
  recorder.add(() => event.emit(event.suite.after, {}));
36
50
  recorder.add(() => event.emit(event.all.result, {}));
37
51
  recorder.add(() => codecept.teardown());
@@ -4,7 +4,7 @@ const output = require('../output');
4
4
  const event = require('../event');
5
5
  const Workers = require('../workers');
6
6
 
7
- module.exports = async function (workerCount, options) {
7
+ module.exports = async function (workerCount, selectedRuns, options) {
8
8
  process.env.profile = options.profile;
9
9
 
10
10
  const { config: testConfig, override = '' } = options;
@@ -15,6 +15,7 @@ module.exports = async function (workerCount, options) {
15
15
  by,
16
16
  testConfig,
17
17
  options,
18
+ selectedRuns,
18
19
  };
19
20
 
20
21
  const numberOfWorkers = parseInt(workerCount, 10);
package/lib/container.js CHANGED
@@ -165,7 +165,17 @@ function createHelpers(config) {
165
165
  } else {
166
166
  moduleName = `./helper/${helperName}`; // built-in helper
167
167
  }
168
- const HelperClass = require(moduleName);
168
+
169
+ // @ts-ignore
170
+ let HelperClass;
171
+ // check if the helper is the built-in, use the require() syntax.
172
+ if (moduleName.startsWith('./helper/')) {
173
+ HelperClass = require(moduleName);
174
+ } else {
175
+ // check if the new syntax export default HelperName is used and loads the Helper, otherwise loads the module that used old syntax export = HelperName.
176
+ HelperClass = require(moduleName).default || require(moduleName);
177
+ }
178
+
169
179
  if (HelperClass._checkRequirements) {
170
180
  const requirements = HelperClass._checkRequirements();
171
181
  if (requirements) {
@@ -351,8 +361,8 @@ function loadSupportObject(modulePath, supportObjectName) {
351
361
  }
352
362
  }
353
363
  if (typeof obj !== 'function'
354
- && Object.getPrototypeOf(obj) !== Object.prototype
355
- && !Array.isArray(obj)
364
+ && Object.getPrototypeOf(obj) !== Object.prototype
365
+ && !Array.isArray(obj)
356
366
  ) {
357
367
  const methods = getObjectMethods(obj);
358
368
  Object.keys(methods)
@@ -17,6 +17,10 @@ const supportedPlatform = {
17
17
  iOS: 'iOS',
18
18
  };
19
19
 
20
+ const vendorPrefix = {
21
+ appium: 'appium',
22
+ };
23
+
20
24
  /**
21
25
  * Appium helper extends [Webriver](http://codecept.io/helpers/WebDriver/) helper.
22
26
  * It supports all browser methods and also includes special methods for mobile apps testing.
@@ -39,6 +43,7 @@ const supportedPlatform = {
39
43
  *
40
44
  * This helper should be configured in codecept.conf.ts or codecept.conf.js
41
45
  *
46
+ * * `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)
42
47
  * * `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
43
48
  * * `host`: (default: 'localhost') Appium host
44
49
  * * `port`: (default: '4723') Appium port
@@ -116,7 +121,7 @@ const supportedPlatform = {
116
121
  *
117
122
  * ## Access From Helpers
118
123
  *
119
- * Receive a Appium client from a custom helper by accessing `browser` property:
124
+ * Receive Appium client from a custom helper by accessing `browser` property:
120
125
  *
121
126
  * ```js
122
127
  * let browser = this.helpers['Appium'].browser
@@ -135,6 +140,9 @@ class Appium extends Webdriver {
135
140
  super(config);
136
141
 
137
142
  this.isRunning = false;
143
+ if (config.appiumV2 === true) {
144
+ this.appiumV2 = true;
145
+ }
138
146
  this.axios = axios.create();
139
147
 
140
148
  webdriverio = require('webdriverio');
@@ -181,14 +189,22 @@ class Appium extends Webdriver {
181
189
 
182
190
  config.baseUrl = config.url || config.baseUrl;
183
191
  if (config.desiredCapabilities && Object.keys(config.desiredCapabilities).length) {
184
- config.capabilities = config.desiredCapabilities;
192
+ config.capabilities = this.appiumV2 === true ? this._convertAppiumV2Caps(config.desiredCapabilities) : config.desiredCapabilities;
193
+ }
194
+
195
+ if (this.appiumV2) {
196
+ config.capabilities[`${vendorPrefix.appium}:deviceName`] = config[`${vendorPrefix.appium}:device`] || config.capabilities[`${vendorPrefix.appium}:deviceName`];
197
+ config.capabilities[`${vendorPrefix.appium}:browserName`] = config[`${vendorPrefix.appium}:browser`] || config.capabilities[`${vendorPrefix.appium}:browserName`];
198
+ config.capabilities[`${vendorPrefix.appium}:app`] = config[`${vendorPrefix.appium}:app`] || config.capabilities[`${vendorPrefix.appium}:app`];
199
+ 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
200
+ } else {
201
+ config.capabilities.deviceName = config.device || config.capabilities.deviceName;
202
+ config.capabilities.browserName = config.browser || config.capabilities.browserName;
203
+ config.capabilities.app = config.app || config.capabilities.app;
204
+ config.capabilities.tunnelIdentifier = config.tunnelIdentifier || config.capabilities.tunnelIdentifier; // Adding the code to connect to sauce labs via sauce tunnel
185
205
  }
186
206
 
187
- config.capabilities.deviceName = config.device || config.capabilities.deviceName;
188
- config.capabilities.browserName = config.browser || config.capabilities.browserName;
189
- config.capabilities.app = config.app || config.capabilities.app;
190
207
  config.capabilities.platformName = config.platform || config.capabilities.platformName;
191
- config.capabilities.tunnelIdentifier = config.tunnelIdentifier || config.capabilities.tunnelIdentifier; // Adding the code to connect to sauce labs via sauce tunnel
192
208
  config.waitForTimeoutInSeconds = config.waitForTimeout / 1000; // convert to seconds
193
209
 
194
210
  // [CodeceptJS compatible] transform host to hostname
@@ -203,6 +219,10 @@ class Appium extends Webdriver {
203
219
  }
204
220
 
205
221
  this.platform = null;
222
+ if (config.capabilities[`${vendorPrefix.appium}:platformName`]) {
223
+ this.platform = config.capabilities[`${vendorPrefix.appium}:platformName`].toLowerCase();
224
+ }
225
+
206
226
  if (config.capabilities.platformName) {
207
227
  this.platform = config.capabilities.platformName.toLowerCase();
208
228
  }
@@ -210,6 +230,18 @@ class Appium extends Webdriver {
210
230
  return config;
211
231
  }
212
232
 
233
+ _convertAppiumV2Caps(capabilities) {
234
+ const _convertedCaps = {};
235
+ for (const [key, value] of Object.entries(capabilities)) {
236
+ if (!key.startsWith(vendorPrefix.appium)) {
237
+ _convertedCaps[`${vendorPrefix.appium}:${key}`] = value;
238
+ } else {
239
+ _convertedCaps[`${key}`] = value;
240
+ }
241
+ }
242
+ return _convertedCaps;
243
+ }
244
+
213
245
  static _config() {
214
246
  return [{
215
247
  name: 'app',
@@ -229,6 +261,11 @@ class Appium extends Webdriver {
229
261
  }
230
262
 
231
263
  async _startBrowser() {
264
+ if (this.appiumV2 === true) {
265
+ this.options.capabilities = this._convertAppiumV2Caps(this.options.capabilities);
266
+ this.options.desiredCapabilities = this._convertAppiumV2Caps(this.options.desiredCapabilities);
267
+ }
268
+
232
269
  try {
233
270
  if (this.options.multiremote) {
234
271
  this.browser = await webdriverio.multiremote(this.options.multiremote);
@@ -445,6 +482,7 @@ class Appium extends Webdriver {
445
482
  */
446
483
  async checkIfAppIsInstalled(bundleId) {
447
484
  onlyForApps.call(this, supportedPlatform.android);
485
+
448
486
  return this.browser.isAppInstalled(bundleId);
449
487
  }
450
488
 
@@ -481,7 +519,7 @@ class Appium extends Webdriver {
481
519
  async seeAppIsNotInstalled(bundleId) {
482
520
  onlyForApps.call(this, supportedPlatform.android);
483
521
  const res = await this.browser.isAppInstalled(bundleId);
484
- return truth(`app ${bundleId}`, 'to be installed').negate(res);
522
+ return truth(`app ${bundleId}`, 'not to be installed').negate(res);
485
523
  }
486
524
 
487
525
  /**
@@ -300,16 +300,16 @@ class JSONResponse extends Helper {
300
300
  *
301
301
  * I.seeResponseMatchesJsonSchema(joi => {
302
302
  * return joi.object({
303
- * name: joi.string();
304
- * id: joi.number();
303
+ * name: joi.string(),
304
+ * id: joi.number()
305
305
  * })
306
306
  * });
307
307
  *
308
308
  * // or pass a valid schema
309
- * const joi = require('joi);
309
+ * const joi = require('joi');
310
310
  *
311
311
  * I.seeResponseMatchesJsonSchema(joi.object({
312
- * name: joi.string();
312
+ * name: joi.string(),
313
313
  * id: joi.number();
314
314
  * });
315
315
  * ```
@@ -694,7 +694,7 @@ class Nightmare extends Helper {
694
694
  async appendField(field, value) {
695
695
  const el = await findField.call(this, field);
696
696
  assertElementExists(el, field, 'Field');
697
- return this.browser.enterText(el, value, false)
697
+ return this.browser.enterText(el, value.toString(), false)
698
698
  .wait(this.options.waitForAction);
699
699
  }
700
700