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/pause.js
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
const colors = require('chalk');
|
|
2
2
|
const readline = require('readline');
|
|
3
|
+
const ora = require('ora-classic');
|
|
4
|
+
const debug = require('debug')('codeceptjs:pause');
|
|
3
5
|
|
|
4
6
|
const container = require('./container');
|
|
5
7
|
const history = require('./history');
|
|
6
8
|
const store = require('./store');
|
|
9
|
+
const AiAssistant = require('./ai');
|
|
7
10
|
const recorder = require('./recorder');
|
|
8
11
|
const event = require('./event');
|
|
9
12
|
const output = require('./output');
|
|
@@ -15,6 +18,8 @@ let nextStep;
|
|
|
15
18
|
let finish;
|
|
16
19
|
let next;
|
|
17
20
|
let registeredVariables = {};
|
|
21
|
+
const aiAssistant = new AiAssistant();
|
|
22
|
+
|
|
18
23
|
/**
|
|
19
24
|
* Pauses test execution and starts interactive shell
|
|
20
25
|
*/
|
|
@@ -45,6 +50,14 @@ function pauseSession(passedObject = {}) {
|
|
|
45
50
|
output.print(colors.yellow(` - Press ${colors.bold('TAB')} twice to see all available commands`));
|
|
46
51
|
output.print(colors.yellow(` - Type ${colors.bold('exit')} + Enter to exit the interactive shell`));
|
|
47
52
|
output.print(colors.yellow(` - Prefix ${colors.bold('=>')} to run js commands ${colors.bold(vars)}`));
|
|
53
|
+
|
|
54
|
+
if (aiAssistant.isEnabled) {
|
|
55
|
+
output.print(colors.blue(` ${colors.bold('OpenAI is enabled! (experimental)')} Write what you want and make OpenAI run it`));
|
|
56
|
+
output.print(colors.blue(' Please note, only HTML fragments with interactive elements are sent to OpenAI'));
|
|
57
|
+
output.print(colors.blue(' Ideas: ask it to fill forms for you or to click'));
|
|
58
|
+
} else {
|
|
59
|
+
output.print(colors.blue(` Enable OpenAI assistant by setting ${colors.bold('OPENAI_API_KEY')} env variable`));
|
|
60
|
+
}
|
|
48
61
|
}
|
|
49
62
|
rl = readline.createInterface(process.stdin, process.stdout, completer);
|
|
50
63
|
|
|
@@ -59,9 +72,10 @@ function pauseSession(passedObject = {}) {
|
|
|
59
72
|
}
|
|
60
73
|
|
|
61
74
|
/* eslint-disable */
|
|
62
|
-
function parseInput(cmd) {
|
|
75
|
+
async function parseInput(cmd) {
|
|
63
76
|
rl.pause();
|
|
64
77
|
next = false;
|
|
78
|
+
recorder.session.start('pause');
|
|
65
79
|
store.debugMode = false;
|
|
66
80
|
if (cmd === '') next = true;
|
|
67
81
|
if (!cmd || cmd === 'resume' || cmd === 'exit') {
|
|
@@ -74,37 +88,78 @@ function parseInput(cmd) {
|
|
|
74
88
|
for (const k of Object.keys(registeredVariables)) {
|
|
75
89
|
eval(`var ${k} = registeredVariables['${k}'];`); // eslint-disable-line no-eval
|
|
76
90
|
}
|
|
91
|
+
|
|
92
|
+
let executeCommand = Promise.resolve();
|
|
93
|
+
|
|
94
|
+
const getCmd = () => {
|
|
95
|
+
debug('Command:', cmd)
|
|
96
|
+
return cmd;
|
|
97
|
+
};
|
|
98
|
+
|
|
77
99
|
store.debugMode = true;
|
|
78
100
|
let isCustomCommand = false;
|
|
79
101
|
let lastError = null;
|
|
102
|
+
let isAiCommand = false;
|
|
103
|
+
let $res;
|
|
80
104
|
try {
|
|
81
105
|
const locate = global.locate; // enable locate in this context
|
|
82
106
|
const I = container.support('I');
|
|
83
107
|
if (cmd.trim().startsWith('=>')) {
|
|
84
108
|
isCustomCommand = true;
|
|
85
109
|
cmd = cmd.trim().substring(2, cmd.length);
|
|
110
|
+
} else if (aiAssistant.isEnabled && !cmd.match(/^\w+\(/) && cmd.includes(' ')) {
|
|
111
|
+
const currentOutputLevel = output.level();
|
|
112
|
+
output.level(0);
|
|
113
|
+
const res = I.grabSource();
|
|
114
|
+
isAiCommand = true;
|
|
115
|
+
executeCommand = executeCommand.then(async () => {
|
|
116
|
+
try {
|
|
117
|
+
const html = await res;
|
|
118
|
+
aiAssistant.setHtmlContext(html);
|
|
119
|
+
} catch (err) {
|
|
120
|
+
output.print(output.styles.error(' ERROR '), 'Can\'t get HTML context', err.stack);
|
|
121
|
+
return;
|
|
122
|
+
} finally {
|
|
123
|
+
output.level(currentOutputLevel);
|
|
124
|
+
}
|
|
125
|
+
// aiAssistant.mockResponse("```js\nI.click('Sign in');\n```");
|
|
126
|
+
const spinner = ora("Processing OpenAI request...").start();
|
|
127
|
+
cmd = await aiAssistant.writeSteps(cmd);
|
|
128
|
+
spinner.stop();
|
|
129
|
+
output.print('');
|
|
130
|
+
output.print(colors.blue(aiAssistant.getResponse()));
|
|
131
|
+
output.print('');
|
|
132
|
+
return cmd;
|
|
133
|
+
})
|
|
86
134
|
} else {
|
|
87
135
|
cmd = `I.${cmd}`;
|
|
88
136
|
}
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
if (isCustomCommand) {
|
|
94
|
-
console.log(val);
|
|
95
|
-
return;
|
|
96
|
-
}
|
|
97
|
-
if (cmd.startsWith('I.see') || cmd.startsWith('I.dontSee')) {
|
|
98
|
-
output.print(output.styles.success(' OK '), cmd);
|
|
99
|
-
return;
|
|
100
|
-
}
|
|
101
|
-
if (cmd.startsWith('I.grab')) {
|
|
102
|
-
output.print(output.styles.debug(val));
|
|
103
|
-
}
|
|
137
|
+
executeCommand = executeCommand.then(async () => {
|
|
138
|
+
const cmd = getCmd();
|
|
139
|
+
if (!cmd) return;
|
|
140
|
+
return eval(cmd); // eslint-disable-line no-eval
|
|
104
141
|
}).catch((err) => {
|
|
142
|
+
debug(err);
|
|
143
|
+
if (isAiCommand) return;
|
|
105
144
|
if (!lastError) output.print(output.styles.error(' ERROR '), err.message);
|
|
145
|
+
debug(err.stack)
|
|
146
|
+
|
|
106
147
|
lastError = err.message;
|
|
107
|
-
})
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
const val = await executeCommand;
|
|
151
|
+
|
|
152
|
+
if (isCustomCommand) {
|
|
153
|
+
if (val !== undefined) console.log('Result', '$res=', val); // eslint-disable-line
|
|
154
|
+
$res = val;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (cmd?.startsWith('I.see') || cmd?.startsWith('I.dontSee')) {
|
|
158
|
+
output.print(output.styles.success(' OK '), cmd);
|
|
159
|
+
}
|
|
160
|
+
if (cmd?.startsWith('I.grab')) {
|
|
161
|
+
output.print(output.styles.debug(val));
|
|
162
|
+
}
|
|
108
163
|
|
|
109
164
|
history.push(cmd); // add command to history when successful
|
|
110
165
|
} catch (err) {
|
|
@@ -117,6 +172,7 @@ function parseInput(cmd) {
|
|
|
117
172
|
// pop latest command from history because it failed
|
|
118
173
|
history.pop();
|
|
119
174
|
|
|
175
|
+
if (isAiCommand) return;
|
|
120
176
|
if (!lastError) output.print(output.styles.error(' FAIL '), msg);
|
|
121
177
|
lastError = err.message;
|
|
122
178
|
});
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
const Container = require('../container');
|
|
2
|
+
const recorder = require('../recorder');
|
|
3
|
+
const event = require('../event');
|
|
4
|
+
const supportedHelpers = require('./standardActingHelpers');
|
|
5
|
+
const { scanForErrorMessages } = require('../html');
|
|
6
|
+
const { output } = require('..');
|
|
7
|
+
|
|
8
|
+
const defaultConfig = {
|
|
9
|
+
errorClasses: ['error', 'warning', 'alert', 'danger'],
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Prints errors found in HTML code after each failed test.
|
|
14
|
+
*
|
|
15
|
+
* It scans HTML and searches for elements with error classes.
|
|
16
|
+
* If an element found prints a text from it to console and adds as artifact to the test.
|
|
17
|
+
*
|
|
18
|
+
* Enable this plugin in config:
|
|
19
|
+
*
|
|
20
|
+
* ```js
|
|
21
|
+
* plugins: {
|
|
22
|
+
* debugErrors: {
|
|
23
|
+
* enabled: true,
|
|
24
|
+
* }
|
|
25
|
+
* ```
|
|
26
|
+
*
|
|
27
|
+
* Additional config options:
|
|
28
|
+
*
|
|
29
|
+
* * `errorClasses` - list of classes to search for errors (default: `['error', 'warning', 'alert', 'danger']`)
|
|
30
|
+
*
|
|
31
|
+
*/
|
|
32
|
+
module.exports = function (config = {}) {
|
|
33
|
+
const helpers = Container.helpers();
|
|
34
|
+
let helper;
|
|
35
|
+
|
|
36
|
+
config = Object.assign(defaultConfig, config);
|
|
37
|
+
|
|
38
|
+
for (const helperName of supportedHelpers) {
|
|
39
|
+
if (Object.keys(helpers).indexOf(helperName) > -1) {
|
|
40
|
+
helper = helpers[helperName];
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (!helper) return; // no helpers for screenshot
|
|
45
|
+
|
|
46
|
+
event.dispatcher.on(event.test.failed, (test) => {
|
|
47
|
+
recorder.add('HTML snapshot failed test', async () => {
|
|
48
|
+
try {
|
|
49
|
+
const currentOutputLevel = output.level();
|
|
50
|
+
output.level(0);
|
|
51
|
+
const html = await helper.grabHTMLFrom('body');
|
|
52
|
+
output.level(currentOutputLevel);
|
|
53
|
+
|
|
54
|
+
if (!html) return;
|
|
55
|
+
|
|
56
|
+
const errors = scanForErrorMessages(html, config.errorClasses);
|
|
57
|
+
if (errors.length) {
|
|
58
|
+
output.debug('Detected errors in HTML code');
|
|
59
|
+
errors.forEach((error) => output.debug(error));
|
|
60
|
+
test.artifacts.errors = errors;
|
|
61
|
+
}
|
|
62
|
+
} catch (err) {
|
|
63
|
+
// not really needed
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
};
|
|
@@ -2,20 +2,18 @@ const { faker } = require('@faker-js/faker');
|
|
|
2
2
|
const transform = require('../transform');
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
|
-
* Use the
|
|
6
|
-
*
|
|
7
|
-
* 
|
|
5
|
+
* Use the `@faker-js/faker` package to generate fake data inside examples on your gherkin tests
|
|
8
6
|
*
|
|
9
7
|
* #### Usage
|
|
10
8
|
*
|
|
11
|
-
* To start please install
|
|
9
|
+
* To start please install `@faker-js/faker` package
|
|
12
10
|
*
|
|
13
11
|
* ```
|
|
14
|
-
* npm install -D faker
|
|
12
|
+
* npm install -D @faker-js/faker
|
|
15
13
|
* ```
|
|
16
14
|
*
|
|
17
15
|
* ```
|
|
18
|
-
* yarn add -D faker
|
|
16
|
+
* yarn add -D @faker-js/faker
|
|
19
17
|
* ```
|
|
20
18
|
*
|
|
21
19
|
* Add this plugin to config file:
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
const debug = require('debug')('codeceptjs:heal');
|
|
2
|
+
const colors = require('chalk');
|
|
3
|
+
const Container = require('../container');
|
|
4
|
+
const AiAssistant = require('../ai');
|
|
5
|
+
const recorder = require('../recorder');
|
|
6
|
+
const event = require('../event');
|
|
7
|
+
const output = require('../output');
|
|
8
|
+
const supportedHelpers = require('./standardActingHelpers');
|
|
9
|
+
|
|
10
|
+
const defaultConfig = {
|
|
11
|
+
healLimit: 2,
|
|
12
|
+
healSteps: [
|
|
13
|
+
'click',
|
|
14
|
+
'fillField',
|
|
15
|
+
'appendField',
|
|
16
|
+
'selectOption',
|
|
17
|
+
'attachFile',
|
|
18
|
+
'checkOption',
|
|
19
|
+
'uncheckOption',
|
|
20
|
+
'doubleClick',
|
|
21
|
+
],
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Self-healing tests with OpenAI.
|
|
26
|
+
*
|
|
27
|
+
* This plugin is experimental and requires OpenAI API key.
|
|
28
|
+
*
|
|
29
|
+
* To use it you need to set OPENAI_API_KEY env variable and enable plugin inside the config.
|
|
30
|
+
*
|
|
31
|
+
* ```js
|
|
32
|
+
* plugins: {
|
|
33
|
+
* heal: {
|
|
34
|
+
* enabled: true,
|
|
35
|
+
* }
|
|
36
|
+
* }
|
|
37
|
+
* ```
|
|
38
|
+
*
|
|
39
|
+
* More config options are available:
|
|
40
|
+
*
|
|
41
|
+
* * `healLimit` - how many steps can be healed in a single test (default: 2)
|
|
42
|
+
* * `healSteps` - which steps can be healed (default: all steps that interact with UI, see list below)
|
|
43
|
+
*
|
|
44
|
+
* Steps to heal:
|
|
45
|
+
*
|
|
46
|
+
* * `click`
|
|
47
|
+
* * `fillField`
|
|
48
|
+
* * `appendField`
|
|
49
|
+
* * `selectOption`
|
|
50
|
+
* * `attachFile`
|
|
51
|
+
* * `checkOption`
|
|
52
|
+
* * `uncheckOption`
|
|
53
|
+
* * `doubleClick`
|
|
54
|
+
*
|
|
55
|
+
*/
|
|
56
|
+
module.exports = function (config = {}) {
|
|
57
|
+
const aiAssistant = new AiAssistant();
|
|
58
|
+
|
|
59
|
+
let currentTest = null;
|
|
60
|
+
let currentStep = null;
|
|
61
|
+
let healedSteps = 0;
|
|
62
|
+
|
|
63
|
+
const healSuggestions = [];
|
|
64
|
+
|
|
65
|
+
config = Object.assign(defaultConfig, config);
|
|
66
|
+
|
|
67
|
+
event.dispatcher.on(event.test.before, (test) => {
|
|
68
|
+
currentTest = test;
|
|
69
|
+
healedSteps = 0;
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
event.dispatcher.on(event.step.started, step => currentStep = step);
|
|
73
|
+
|
|
74
|
+
event.dispatcher.on(event.step.before, () => {
|
|
75
|
+
const store = require('../store');
|
|
76
|
+
if (store.debugMode) return;
|
|
77
|
+
|
|
78
|
+
recorder.catchWithoutStop(async (err) => {
|
|
79
|
+
if (!aiAssistant.isEnabled) throw err;
|
|
80
|
+
if (!currentStep) throw err;
|
|
81
|
+
if (!config.healSteps.includes(currentStep.name)) throw err;
|
|
82
|
+
const test = currentTest;
|
|
83
|
+
|
|
84
|
+
if (healedSteps >= config.healLimit) {
|
|
85
|
+
output.print(colors.bold.red(`Can't heal more than ${config.healLimit} step(s) in a test`));
|
|
86
|
+
output.print('Entire flow can be broken, please check it manually');
|
|
87
|
+
output.print('or increase healing limit in heal plugin config');
|
|
88
|
+
|
|
89
|
+
throw err;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
recorder.session.start('heal');
|
|
93
|
+
const helpers = Container.helpers();
|
|
94
|
+
let helper;
|
|
95
|
+
|
|
96
|
+
for (const helperName of supportedHelpers) {
|
|
97
|
+
if (Object.keys(helpers).indexOf(helperName) > -1) {
|
|
98
|
+
helper = helpers[helperName];
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (!helper) throw err; // no helpers for html
|
|
103
|
+
|
|
104
|
+
const step = test.steps[test.steps.length - 1];
|
|
105
|
+
debug('Self-healing started', step.toCode());
|
|
106
|
+
|
|
107
|
+
const currentOutputLevel = output.level();
|
|
108
|
+
output.level(0);
|
|
109
|
+
const html = await helper.grabHTMLFrom('body');
|
|
110
|
+
output.level(currentOutputLevel);
|
|
111
|
+
|
|
112
|
+
if (!html) throw err;
|
|
113
|
+
|
|
114
|
+
aiAssistant.setHtmlContext(html);
|
|
115
|
+
await tryToHeal(step, err);
|
|
116
|
+
recorder.session.restore();
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
event.dispatcher.on(event.all.result, () => {
|
|
121
|
+
if (!healSuggestions.length) return;
|
|
122
|
+
|
|
123
|
+
const { print } = output;
|
|
124
|
+
|
|
125
|
+
print('');
|
|
126
|
+
print('===================');
|
|
127
|
+
print(colors.bold.green('Self-Healing Report:'));
|
|
128
|
+
|
|
129
|
+
print(`${colors.bold(healSuggestions.length)} step(s) were healed by AI`);
|
|
130
|
+
|
|
131
|
+
let i = 1;
|
|
132
|
+
print('');
|
|
133
|
+
print('Suggested changes:');
|
|
134
|
+
print('');
|
|
135
|
+
|
|
136
|
+
for (const suggestion of healSuggestions) {
|
|
137
|
+
print(`${i}. To fix ${colors.bold.blue(suggestion.test.title)}`);
|
|
138
|
+
print('Replace the failed code with:');
|
|
139
|
+
print(colors.red(`- ${suggestion.step.toCode()}`));
|
|
140
|
+
print(colors.green(`+ ${suggestion.snippet}`));
|
|
141
|
+
print(suggestion.step.line());
|
|
142
|
+
print('');
|
|
143
|
+
i++;
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
async function tryToHeal(failedStep, err) {
|
|
148
|
+
output.debug(`Running OpenAI to heal ${failedStep.toCode()} step`);
|
|
149
|
+
|
|
150
|
+
const codeSnippets = await aiAssistant.healFailedStep(
|
|
151
|
+
failedStep, err, currentTest,
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
output.debug(`Received ${codeSnippets.length} suggestions from OpenAI`);
|
|
155
|
+
|
|
156
|
+
for (const codeSnippet of codeSnippets) {
|
|
157
|
+
try {
|
|
158
|
+
debug('Executing', codeSnippet);
|
|
159
|
+
await eval(codeSnippet); // eslint-disable-line
|
|
160
|
+
|
|
161
|
+
healSuggestions.push({
|
|
162
|
+
test: currentTest,
|
|
163
|
+
step: failedStep,
|
|
164
|
+
snippet: codeSnippet,
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
output.print(colors.bold.green(' Code healed successfully'));
|
|
168
|
+
healedSteps++;
|
|
169
|
+
return;
|
|
170
|
+
} catch (err) {
|
|
171
|
+
debug('Failed to execute code', err);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
output.debug(`Couldn't heal the code for ${failedStep.toCode()}`);
|
|
176
|
+
}
|
|
177
|
+
};
|
|
@@ -75,7 +75,8 @@ module.exports = function (config) {
|
|
|
75
75
|
event.dispatcher.on(event.test.failed, (test) => {
|
|
76
76
|
recorder.add('screenshot of failed test', async () => {
|
|
77
77
|
let fileName = clearString(test.title);
|
|
78
|
-
|
|
78
|
+
const dataType = 'image/png';
|
|
79
|
+
// This prevents data driven to be included in the failed screenshot file name
|
|
79
80
|
if (fileName.indexOf('{') !== -1) {
|
|
80
81
|
fileName = fileName.substr(0, (fileName.indexOf('{') - 3)).trim();
|
|
81
82
|
}
|
|
@@ -106,7 +107,15 @@ module.exports = function (config) {
|
|
|
106
107
|
|
|
107
108
|
const allureReporter = Container.plugins('allure');
|
|
108
109
|
if (allureReporter) {
|
|
109
|
-
allureReporter.addAttachment('Last Seen Screenshot', fs.readFileSync(path.join(global.output_dir, fileName)),
|
|
110
|
+
allureReporter.addAttachment('Main session - Last Seen Screenshot', fs.readFileSync(path.join(global.output_dir, fileName)), dataType);
|
|
111
|
+
|
|
112
|
+
if (helper.activeSessionName) {
|
|
113
|
+
for (const sessionName in helper.sessionPages) {
|
|
114
|
+
const screenshotFileName = `${sessionName}_${fileName}`;
|
|
115
|
+
test.artifacts[`${sessionName.replace(/ /g, '_')}_screenshot`] = path.join(global.output_dir, screenshotFileName);
|
|
116
|
+
allureReporter.addAttachment(`${sessionName} - Last Seen Screenshot`, fs.readFileSync(path.join(global.output_dir, screenshotFileName)), dataType);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
110
119
|
}
|
|
111
120
|
|
|
112
121
|
const cucumberReporter = Container.plugins('cucumberJsonReporter');
|
package/lib/recorder.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
const debug = require('debug')('codeceptjs:recorder');
|
|
2
2
|
const promiseRetry = require('promise-retry');
|
|
3
|
-
|
|
3
|
+
const { printObjectProperties } = require('./utils');
|
|
4
4
|
const { log } = require('./output');
|
|
5
5
|
|
|
6
6
|
const MAX_TASKS = 100;
|
|
@@ -41,6 +41,7 @@ module.exports = {
|
|
|
41
41
|
* @inner
|
|
42
42
|
*/
|
|
43
43
|
start() {
|
|
44
|
+
debug('Starting recording promises');
|
|
44
45
|
running = true;
|
|
45
46
|
asyncErr = null;
|
|
46
47
|
errFn = null;
|
|
@@ -161,7 +162,7 @@ module.exports = {
|
|
|
161
162
|
* true: it will retries if `retryOpts` set.
|
|
162
163
|
* false: ignore `retryOpts` and won't retry.
|
|
163
164
|
* @param {number} [timeout]
|
|
164
|
-
* @return {Promise<*>
|
|
165
|
+
* @return {Promise<*>}
|
|
165
166
|
* @inner
|
|
166
167
|
*/
|
|
167
168
|
add(taskName, fn = undefined, force = false, retry = undefined, timeout = undefined) {
|
|
@@ -175,7 +176,7 @@ module.exports = {
|
|
|
175
176
|
return;
|
|
176
177
|
}
|
|
177
178
|
tasks.push(taskName);
|
|
178
|
-
|
|
179
|
+
debug(`${currentQueue()}Queued | ${taskName}`);
|
|
179
180
|
|
|
180
181
|
return promise = Promise.resolve(promise).then((res) => {
|
|
181
182
|
// prefer options for non-conditional retries
|
|
@@ -224,10 +225,11 @@ module.exports = {
|
|
|
224
225
|
* @inner
|
|
225
226
|
*/
|
|
226
227
|
catch(customErrFn) {
|
|
228
|
+
debug(`${currentQueue()}Queued | catch with error handler`);
|
|
227
229
|
return promise = promise.catch((err) => {
|
|
228
230
|
log(`${currentQueue()}Error | ${err}`);
|
|
229
231
|
if (!(err instanceof Error)) { // strange things may happen
|
|
230
|
-
err = new Error(`[Wrapped Error] ${
|
|
232
|
+
err = new Error(`[Wrapped Error] ${printObjectProperties(err)}`); // we should be prepared for them
|
|
231
233
|
}
|
|
232
234
|
if (customErrFn) {
|
|
233
235
|
customErrFn(err);
|
|
@@ -250,9 +252,9 @@ module.exports = {
|
|
|
250
252
|
err = new Error(`[Wrapped Error] ${JSON.stringify(err)}`); // we should be prepared for them
|
|
251
253
|
}
|
|
252
254
|
if (customErrFn) {
|
|
253
|
-
customErrFn(err);
|
|
254
|
-
}
|
|
255
|
-
errFn(err);
|
|
255
|
+
return customErrFn(err);
|
|
256
|
+
} if (errFn) {
|
|
257
|
+
return errFn(err);
|
|
256
258
|
}
|
|
257
259
|
});
|
|
258
260
|
},
|
|
@@ -264,8 +266,9 @@ module.exports = {
|
|
|
264
266
|
* @param {*} err
|
|
265
267
|
* @inner
|
|
266
268
|
*/
|
|
269
|
+
|
|
267
270
|
throw(err) {
|
|
268
|
-
return this.add(`throw error ${err}`, () => {
|
|
271
|
+
return this.add(`throw error: ${err.message}`, () => {
|
|
269
272
|
throw err;
|
|
270
273
|
});
|
|
271
274
|
},
|
package/lib/secret.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
/* eslint-disable max-classes-per-file */
|
|
2
2
|
const { deepClone } = require('./utils');
|
|
3
3
|
|
|
4
|
+
const maskedString = '*****';
|
|
5
|
+
|
|
4
6
|
/** @param {string} secret */
|
|
5
7
|
class Secret {
|
|
6
8
|
constructor(secret) {
|
|
@@ -13,7 +15,7 @@ class Secret {
|
|
|
13
15
|
}
|
|
14
16
|
|
|
15
17
|
getMasked() {
|
|
16
|
-
return
|
|
18
|
+
return maskedString;
|
|
17
19
|
}
|
|
18
20
|
|
|
19
21
|
/**
|
|
@@ -36,12 +38,11 @@ function secretObject(obj, fieldsToHide = []) {
|
|
|
36
38
|
if (prop === 'toString') {
|
|
37
39
|
return function () {
|
|
38
40
|
const maskedObject = deepClone(obj);
|
|
39
|
-
fieldsToHide.forEach(f => maskedObject[f] =
|
|
41
|
+
fieldsToHide.forEach(f => maskedObject[f] = maskedString);
|
|
40
42
|
return JSON.stringify(maskedObject);
|
|
41
43
|
};
|
|
42
44
|
}
|
|
43
|
-
|
|
44
|
-
return obj[prop];
|
|
45
|
+
return fieldsToHide.includes(prop) ? new Secret(obj[prop]) : obj[prop];
|
|
45
46
|
},
|
|
46
47
|
};
|
|
47
48
|
|
package/lib/step.js
CHANGED
|
@@ -172,7 +172,12 @@ class Step {
|
|
|
172
172
|
} else if (arg.toString && arg.toString() !== '[object Object]') {
|
|
173
173
|
return arg.toString();
|
|
174
174
|
} else if (typeof arg === 'object') {
|
|
175
|
-
|
|
175
|
+
const returnedArg = {};
|
|
176
|
+
for (const [key, value] of Object.entries(arg)) {
|
|
177
|
+
returnedArg[key] = value;
|
|
178
|
+
if (value instanceof Secret) returnedArg[key] = value.getMasked();
|
|
179
|
+
}
|
|
180
|
+
return JSON.stringify(returnedArg);
|
|
176
181
|
}
|
|
177
182
|
return arg;
|
|
178
183
|
}).join(', ');
|
package/lib/ui.js
CHANGED
|
@@ -23,10 +23,11 @@ const setContextTranslation = (context) => {
|
|
|
23
23
|
/**
|
|
24
24
|
* Codecept-style interface:
|
|
25
25
|
*
|
|
26
|
-
* Feature('login')
|
|
27
|
-
*
|
|
26
|
+
* Feature('login');
|
|
27
|
+
*
|
|
28
|
+
* Scenario('login as regular user', ({I}) {
|
|
28
29
|
* I.fillField();
|
|
29
|
-
* I.click()
|
|
30
|
+
* I.click();
|
|
30
31
|
* I.see('Hello, '+data.login);
|
|
31
32
|
* });
|
|
32
33
|
*
|
package/lib/utils.js
CHANGED
|
@@ -455,3 +455,20 @@ module.exports.isNotSet = function (obj) {
|
|
|
455
455
|
if (obj === undefined) return true;
|
|
456
456
|
return false;
|
|
457
457
|
};
|
|
458
|
+
|
|
459
|
+
module.exports.emptyFolder = async (directoryPath) => {
|
|
460
|
+
require('child_process').execSync(`rm -rf ${directoryPath}/*`);
|
|
461
|
+
};
|
|
462
|
+
|
|
463
|
+
module.exports.printObjectProperties = (obj) => {
|
|
464
|
+
if (typeof obj !== 'object' || obj === null) {
|
|
465
|
+
return obj;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
let result = '';
|
|
469
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
470
|
+
result += `${key}: "${value}"; `;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
return `{${result}}`;
|
|
474
|
+
};
|