codeceptjs 3.4.1 → 3.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +85 -0
- package/README.md +11 -9
- package/bin/codecept.js +1 -1
- package/docs/ai.md +248 -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 +234 -54
- package/docs/build/Protractor.js +3 -1
- package/docs/build/Puppeteer.js +101 -12
- package/docs/build/REST.js +15 -5
- package/docs/build/TestCafe.js +61 -2
- package/docs/build/WebDriver.js +85 -5
- package/docs/changelog.md +85 -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 +228 -151
- package/docs/helpers/Puppeteer.md +153 -101
- package/docs/helpers/REST.md +6 -5
- package/docs/helpers/TestCafe.md +97 -49
- package/docs/helpers/WebDriver.md +159 -107
- 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 +2 -2
- package/docs/webapi/appendField.mustache +2 -0
- package/docs/webapi/blur.mustache +17 -0
- package/docs/webapi/focus.mustache +12 -0
- package/docs/webapi/type.mustache +3 -0
- package/lib/ai.js +171 -0
- package/lib/cli.js +10 -2
- 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 +23 -1
- package/lib/command/interactive.js +15 -1
- package/lib/command/run-workers.js +2 -1
- package/lib/container.js +13 -3
- package/lib/event.js +2 -0
- 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 +200 -45
- package/lib/helper/Protractor.js +1 -1
- package/lib/helper/Puppeteer.js +67 -12
- package/lib/helper/REST.js +15 -5
- package/lib/helper/TestCafe.js +30 -2
- package/lib/helper/WebDriver.js +51 -5
- package/lib/helper/scripts/blurElement.js +17 -0
- package/lib/helper/scripts/focusElement.js +17 -0
- package/lib/helper/scripts/highlightElement.js +20 -0
- package/lib/html.js +258 -0
- package/lib/interfaces/gherkin.js +8 -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 +177 -0
- package/lib/plugin/screenshotOnFail.js +11 -2
- package/lib/recorder.js +11 -8
- package/lib/secret.js +5 -4
- package/lib/step.js +6 -1
- package/lib/ui.js +4 -3
- package/lib/utils.js +17 -0
- package/lib/workers.js +57 -9
- package/package.json +25 -16
- package/translations/ja-JP.js +9 -9
- package/typings/index.d.ts +43 -9
- package/typings/promiseBasedTypes.d.ts +242 -25
- package/typings/types.d.ts +260 -35
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
|
|
|
@@ -305,6 +317,16 @@ module.exports = function (initPath) {
|
|
|
305
317
|
|
|
306
318
|
print('Configure helpers...');
|
|
307
319
|
inquirer.prompt(helperConfigs).then((helperResult) => {
|
|
320
|
+
if (helperResult.Playwright_browser === 'electron') {
|
|
321
|
+
delete helperResult.Playwright_url;
|
|
322
|
+
delete helperResult.Playwright_show;
|
|
323
|
+
|
|
324
|
+
helperResult.Playwright_electron = {
|
|
325
|
+
executablePath: '// require("electron") or require("electron-forge")',
|
|
326
|
+
args: ['path/to/your/main.js'],
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
|
|
308
330
|
Object.keys(helperResult).forEach((key) => {
|
|
309
331
|
const parts = key.split('_');
|
|
310
332
|
const helperName = parts[0];
|
|
@@ -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/event.js
CHANGED
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
|
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
const Helper = require('@codeceptjs/helper');
|
|
2
|
+
const AiAssistant = require('../ai');
|
|
3
|
+
const standardActingHelpers = require('../plugin/standardActingHelpers');
|
|
4
|
+
const Container = require('../container');
|
|
5
|
+
const { splitByChunks, minifyHtml } = require('../html');
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* OpenAI Helper for CodeceptJS.
|
|
9
|
+
*
|
|
10
|
+
* This helper class provides integration with the OpenAI GPT-3.5 or 4 language model for generating responses to questions or prompts within the context of web pages. It allows you to interact with the GPT-3.5 model to obtain intelligent responses based on HTML fragments or general prompts.
|
|
11
|
+
* This helper should be enabled with any web helpers like Playwright or Puppeteer or WebDrvier to ensure the HTML context is available.
|
|
12
|
+
*
|
|
13
|
+
* ## Configuration
|
|
14
|
+
*
|
|
15
|
+
* This helper should be configured in codecept.json or codecept.conf.js
|
|
16
|
+
*
|
|
17
|
+
* * `chunkSize`: (optional, default: 80000) - The maximum number of characters to send to the OpenAI API at once. We split HTML fragments by 8000 chars to not exceed token limit. Increase this value if you use GPT-4.
|
|
18
|
+
*/
|
|
19
|
+
class OpenAI extends Helper {
|
|
20
|
+
constructor(config) {
|
|
21
|
+
super(config);
|
|
22
|
+
this.aiAssistant = new AiAssistant();
|
|
23
|
+
|
|
24
|
+
this.options = {
|
|
25
|
+
chunkSize: 80000,
|
|
26
|
+
};
|
|
27
|
+
this.options = { ...this.options, ...config };
|
|
28
|
+
|
|
29
|
+
const helpers = Container.helpers();
|
|
30
|
+
|
|
31
|
+
for (const helperName of standardActingHelpers) {
|
|
32
|
+
if (Object.keys(helpers).indexOf(helperName) > -1) {
|
|
33
|
+
this.helper = helpers[helperName];
|
|
34
|
+
break;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Asks the OpenAI GPT language model a question based on the provided prompt within the context of the current page's HTML.
|
|
41
|
+
*
|
|
42
|
+
* ```js
|
|
43
|
+
* I.askGptOnPage('what does this page do?');
|
|
44
|
+
* ```
|
|
45
|
+
*
|
|
46
|
+
* @async
|
|
47
|
+
* @param {string} prompt - The question or prompt to ask the GPT model.
|
|
48
|
+
* @returns {Promise<string>} - A Promise that resolves to the generated responses from the GPT model, joined by newlines.
|
|
49
|
+
*/
|
|
50
|
+
async askGptOnPage(prompt) {
|
|
51
|
+
const html = await this.helper.grabSource();
|
|
52
|
+
|
|
53
|
+
const htmlChunks = splitByChunks(html, this.options.chunkSize);
|
|
54
|
+
|
|
55
|
+
if (htmlChunks.length > 1) this.debug(`Splitting HTML into ${htmlChunks.length} chunks`);
|
|
56
|
+
|
|
57
|
+
const responses = [];
|
|
58
|
+
|
|
59
|
+
for (const chunk of htmlChunks) {
|
|
60
|
+
const messages = [
|
|
61
|
+
{ role: 'user', content: prompt },
|
|
62
|
+
{ role: 'user', content: `Within this HTML: ${minifyHtml(chunk)}` },
|
|
63
|
+
];
|
|
64
|
+
|
|
65
|
+
if (htmlChunks.length > 1) messages.push({ role: 'user', content: 'If action is not possible on this page, do not propose anything, I will send another HTML fragment' });
|
|
66
|
+
|
|
67
|
+
const response = await this.aiAssistant.createCompletion(messages);
|
|
68
|
+
|
|
69
|
+
console.log(response);
|
|
70
|
+
|
|
71
|
+
responses.push(response);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return responses.join('\n\n');
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Asks the OpenAI GPT-3.5 language model a question based on the provided prompt within the context of a specific HTML fragment on the current page.
|
|
79
|
+
*
|
|
80
|
+
* ```js
|
|
81
|
+
* I.askGptOnPageFragment('describe features of this screen', '.screen');
|
|
82
|
+
* ```
|
|
83
|
+
*
|
|
84
|
+
* @async
|
|
85
|
+
* @param {string} prompt - The question or prompt to ask the GPT-3.5 model.
|
|
86
|
+
* @param {string} locator - The locator or selector used to identify the HTML fragment on the page.
|
|
87
|
+
* @returns {Promise<string>} - A Promise that resolves to the generated response from the GPT model.
|
|
88
|
+
*/
|
|
89
|
+
async askGptOnPageFragment(prompt, locator) {
|
|
90
|
+
const html = await this.helper.grabHTMLFrom(locator);
|
|
91
|
+
|
|
92
|
+
const messages = [
|
|
93
|
+
{ role: 'user', content: prompt },
|
|
94
|
+
{ role: 'user', content: `Within this HTML: ${minifyHtml(html)}` },
|
|
95
|
+
];
|
|
96
|
+
|
|
97
|
+
const response = await this.aiAssistant.createCompletion(messages);
|
|
98
|
+
|
|
99
|
+
console.log(response);
|
|
100
|
+
|
|
101
|
+
return response;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Send a general request to ChatGPT and return response.
|
|
106
|
+
* @param {string} prompt
|
|
107
|
+
* @returns {Promise<string>} - A Promise that resolves to the generated response from the GPT model.
|
|
108
|
+
*/
|
|
109
|
+
async askGptGeneralPrompt(prompt) {
|
|
110
|
+
const messages = [
|
|
111
|
+
{ role: 'user', content: prompt },
|
|
112
|
+
];
|
|
113
|
+
|
|
114
|
+
const completion = await this.aiAssistant.createCompletion(messages);
|
|
115
|
+
|
|
116
|
+
const response = completion?.data?.choices[0]?.message?.content;
|
|
117
|
+
|
|
118
|
+
console.log(response);
|
|
119
|
+
|
|
120
|
+
return response;
|
|
121
|
+
}
|
|
122
|
+
}
|