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.
- package/CHANGELOG.md +70 -0
- package/README.md +9 -7
- package/bin/codecept.js +1 -1
- package/docs/ai.md +246 -0
- package/docs/build/Appium.js +47 -7
- package/docs/build/JSONResponse.js +4 -4
- package/docs/build/Nightmare.js +3 -1
- package/docs/build/OpenAI.js +122 -0
- package/docs/build/Playwright.js +193 -45
- package/docs/build/Protractor.js +3 -1
- package/docs/build/Puppeteer.js +45 -12
- package/docs/build/REST.js +15 -5
- package/docs/build/TestCafe.js +3 -1
- package/docs/build/WebDriver.js +30 -5
- package/docs/changelog.md +70 -0
- package/docs/helpers/Appium.md +152 -147
- package/docs/helpers/JSONResponse.md +4 -4
- package/docs/helpers/Nightmare.md +2 -0
- package/docs/helpers/OpenAI.md +70 -0
- package/docs/helpers/Playwright.md +194 -152
- package/docs/helpers/Puppeteer.md +6 -0
- package/docs/helpers/REST.md +6 -5
- package/docs/helpers/TestCafe.md +2 -0
- package/docs/helpers/WebDriver.md +10 -4
- package/docs/mobile.md +49 -2
- package/docs/parallel.md +56 -0
- package/docs/plugins.md +87 -33
- package/docs/secrets.md +6 -0
- package/docs/tutorial.md +5 -5
- package/docs/webapi/appendField.mustache +2 -0
- package/docs/webapi/type.mustache +3 -0
- package/lib/ai.js +171 -0
- package/lib/cli.js +1 -1
- package/lib/codecept.js +4 -0
- package/lib/command/dryRun.js +9 -1
- package/lib/command/generate.js +46 -3
- package/lib/command/init.js +13 -1
- package/lib/command/interactive.js +15 -1
- package/lib/command/run-workers.js +2 -1
- package/lib/container.js +13 -3
- package/lib/helper/Appium.js +45 -7
- package/lib/helper/JSONResponse.js +4 -4
- package/lib/helper/Nightmare.js +1 -1
- package/lib/helper/OpenAI.js +122 -0
- package/lib/helper/Playwright.js +190 -38
- package/lib/helper/Protractor.js +1 -1
- package/lib/helper/Puppeteer.js +40 -12
- package/lib/helper/REST.js +15 -5
- package/lib/helper/TestCafe.js +1 -1
- package/lib/helper/WebDriver.js +25 -5
- package/lib/helper/scripts/highlightElement.js +20 -0
- package/lib/html.js +258 -0
- package/lib/listener/retry.js +2 -1
- package/lib/pause.js +73 -17
- package/lib/plugin/debugErrors.js +67 -0
- package/lib/plugin/fakerTransform.js +4 -6
- package/lib/plugin/heal.js +179 -0
- package/lib/plugin/screenshotOnFail.js +11 -2
- package/lib/plugin/wdio.js +4 -12
- package/lib/recorder.js +4 -4
- package/lib/scenario.js +6 -4
- package/lib/secret.js +5 -4
- package/lib/step.js +6 -1
- package/lib/ui.js +4 -3
- package/lib/utils.js +4 -0
- package/lib/workers.js +57 -9
- package/package.json +26 -14
- package/translations/ja-JP.js +9 -9
- package/typings/index.d.ts +43 -9
- package/typings/promiseBasedTypes.d.ts +124 -24
- 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
|
|
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
|
}
|
package/lib/command/dryRun.js
CHANGED
|
@@ -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
|
-
|
|
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) {
|
package/lib/command/generate.js
CHANGED
|
@@ -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
|
-
}
|
|
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
|
-
|
|
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
|
|
package/lib/command/init.js
CHANGED
|
@@ -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.
|
|
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
|
-
|
|
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
|
-
|
|
355
|
-
|
|
364
|
+
&& Object.getPrototypeOf(obj) !== Object.prototype
|
|
365
|
+
&& !Array.isArray(obj)
|
|
356
366
|
) {
|
|
357
367
|
const methods = getObjectMethods(obj);
|
|
358
368
|
Object.keys(methods)
|
package/lib/helper/Appium.js
CHANGED
|
@@ -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
|
|
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
|
* ```
|
package/lib/helper/Nightmare.js
CHANGED
|
@@ -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
|
|