codeceptjs 3.6.10 → 3.7.0-beta.2
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/README.md +81 -110
- package/bin/codecept.js +2 -2
- package/docs/webapi/clearCookie.mustache +1 -1
- package/lib/actor.js +46 -36
- package/lib/assert/empty.js +3 -5
- package/lib/assert/equal.js +4 -7
- package/lib/assert/include.js +4 -6
- package/lib/assert/throws.js +2 -4
- package/lib/assert/truth.js +2 -2
- package/lib/codecept.js +87 -83
- package/lib/command/configMigrate.js +2 -4
- package/lib/command/definitions.js +5 -25
- package/lib/command/generate.js +10 -14
- package/lib/command/gherkin/snippets.js +10 -8
- package/lib/command/gherkin/steps.js +1 -1
- package/lib/command/info.js +1 -3
- package/lib/command/init.js +8 -12
- package/lib/command/interactive.js +1 -1
- package/lib/command/list.js +1 -1
- package/lib/command/run-multiple.js +12 -35
- package/lib/command/run-workers.js +10 -10
- package/lib/command/utils.js +5 -6
- package/lib/command/workers/runTests.js +14 -17
- package/lib/container.js +327 -237
- package/lib/data/context.js +10 -13
- package/lib/data/dataScenarioConfig.js +8 -8
- package/lib/data/dataTableArgument.js +6 -6
- package/lib/data/table.js +5 -11
- package/lib/els.js +177 -0
- package/lib/event.js +1 -0
- package/lib/heal.js +78 -80
- package/lib/helper/ApiDataFactory.js +3 -6
- package/lib/helper/Appium.js +15 -30
- package/lib/helper/FileSystem.js +3 -3
- package/lib/helper/GraphQLDataFactory.js +3 -3
- package/lib/helper/JSONResponse.js +57 -37
- package/lib/helper/Nightmare.js +35 -53
- package/lib/helper/Playwright.js +189 -251
- package/lib/helper/Protractor.js +54 -77
- package/lib/helper/Puppeteer.js +134 -232
- package/lib/helper/REST.js +5 -17
- package/lib/helper/TestCafe.js +21 -44
- package/lib/helper/WebDriver.js +103 -162
- package/lib/helper/testcafe/testcafe-utils.js +26 -27
- package/lib/listener/artifacts.js +2 -2
- package/lib/listener/emptyRun.js +58 -0
- package/lib/listener/exit.js +4 -4
- package/lib/listener/{retry.js → globalRetry.js} +5 -5
- package/lib/listener/{timeout.js → globalTimeout.js} +9 -8
- package/lib/listener/helpers.js +15 -15
- package/lib/listener/mocha.js +1 -1
- package/lib/listener/steps.js +17 -12
- package/lib/listener/store.js +12 -0
- package/lib/mocha/asyncWrapper.js +204 -0
- package/lib/{interfaces → mocha}/bdd.js +3 -3
- package/lib/mocha/cli.js +257 -0
- package/lib/mocha/factory.js +104 -0
- package/lib/{interfaces → mocha}/featureConfig.js +11 -12
- package/lib/{interfaces → mocha}/gherkin.js +26 -28
- package/lib/mocha/hooks.js +83 -0
- package/lib/mocha/index.js +12 -0
- package/lib/mocha/inject.js +24 -0
- package/lib/{interfaces → mocha}/scenarioConfig.js +10 -6
- package/lib/mocha/suite.js +55 -0
- package/lib/mocha/test.js +60 -0
- package/lib/mocha/types.d.ts +31 -0
- package/lib/mocha/ui.js +219 -0
- package/lib/output.js +28 -10
- package/lib/pause.js +159 -135
- package/lib/plugin/autoDelay.js +4 -4
- package/lib/plugin/autoLogin.js +6 -7
- package/lib/plugin/commentStep.js +1 -1
- package/lib/plugin/coverage.js +10 -19
- package/lib/plugin/customLocator.js +3 -3
- package/lib/plugin/debugErrors.js +2 -2
- package/lib/plugin/eachElement.js +1 -1
- package/lib/plugin/fakerTransform.js +1 -1
- package/lib/plugin/heal.js +6 -9
- package/lib/plugin/retryFailedStep.js +4 -4
- package/lib/plugin/retryTo.js +2 -2
- package/lib/plugin/screenshotOnFail.js +9 -36
- package/lib/plugin/selenoid.js +15 -35
- package/lib/plugin/stepByStepReport.js +51 -13
- package/lib/plugin/stepTimeout.js +4 -11
- package/lib/plugin/subtitles.js +4 -4
- package/lib/plugin/tryTo.js +1 -1
- package/lib/plugin/wdio.js +8 -10
- package/lib/recorder.js +142 -121
- package/lib/secret.js +1 -1
- package/lib/step.js +160 -144
- package/lib/store.js +6 -2
- package/lib/template/heal.js +2 -11
- package/lib/utils.js +224 -216
- package/lib/within.js +73 -55
- package/lib/workers.js +265 -261
- package/package.json +46 -47
- package/typings/index.d.ts +172 -184
- package/typings/promiseBasedTypes.d.ts +95 -516
- package/typings/types.d.ts +169 -587
- package/lib/cli.js +0 -256
- package/lib/helper/ExpectHelper.js +0 -391
- package/lib/helper/SoftExpectHelper.js +0 -381
- package/lib/mochaFactory.js +0 -113
- package/lib/scenario.js +0 -224
- package/lib/ui.js +0 -236
package/lib/data/context.js
CHANGED
|
@@ -13,8 +13,8 @@ module.exports = function (context) {
|
|
|
13
13
|
fn = opts
|
|
14
14
|
opts = {}
|
|
15
15
|
}
|
|
16
|
-
opts.data = data.map(
|
|
17
|
-
data.forEach(
|
|
16
|
+
opts.data = data.map(dataRow => dataRow.data)
|
|
17
|
+
data.forEach(dataRow => {
|
|
18
18
|
const dataTitle = replaceTitle(title, dataRow)
|
|
19
19
|
if (dataRow.skip) {
|
|
20
20
|
context.xScenario(dataTitle)
|
|
@@ -32,8 +32,8 @@ module.exports = function (context) {
|
|
|
32
32
|
fn = opts
|
|
33
33
|
opts = {}
|
|
34
34
|
}
|
|
35
|
-
opts.data = data.map(
|
|
36
|
-
data.forEach(
|
|
35
|
+
opts.data = data.map(dataRow => dataRow.data)
|
|
36
|
+
data.forEach(dataRow => {
|
|
37
37
|
const dataTitle = replaceTitle(title, dataRow)
|
|
38
38
|
if (dataRow.skip) {
|
|
39
39
|
context.xScenario(dataTitle)
|
|
@@ -51,8 +51,8 @@ module.exports = function (context) {
|
|
|
51
51
|
context.xData = function (dataTable) {
|
|
52
52
|
const data = detectDataType(dataTable)
|
|
53
53
|
return {
|
|
54
|
-
Scenario:
|
|
55
|
-
data.forEach(
|
|
54
|
+
Scenario: title => {
|
|
55
|
+
data.forEach(dataRow => {
|
|
56
56
|
const dataTitle = replaceTitle(title, dataRow)
|
|
57
57
|
context.xScenario(dataTitle)
|
|
58
58
|
})
|
|
@@ -69,10 +69,7 @@ function replaceTitle(title, dataRow) {
|
|
|
69
69
|
|
|
70
70
|
// if `dataRow` is object and has own `toString()` method,
|
|
71
71
|
// it should be printed
|
|
72
|
-
if (
|
|
73
|
-
Object.prototype.toString.call(dataRow.data) === Object().toString() &&
|
|
74
|
-
dataRow.data.toString() !== Object().toString()
|
|
75
|
-
) {
|
|
72
|
+
if (Object.prototype.toString.call(dataRow.data) === Object().toString() && dataRow.data.toString() !== Object().toString()) {
|
|
76
73
|
return `${title} | ${dataRow.data}`
|
|
77
74
|
}
|
|
78
75
|
|
|
@@ -102,7 +99,7 @@ function detectDataType(dataTable) {
|
|
|
102
99
|
return dataTable()
|
|
103
100
|
}
|
|
104
101
|
if (Array.isArray(dataTable)) {
|
|
105
|
-
return dataTable.map(
|
|
102
|
+
return dataTable.map(item => {
|
|
106
103
|
if (isTableDataRow(item)) {
|
|
107
104
|
return item
|
|
108
105
|
}
|
|
@@ -117,10 +114,10 @@ function detectDataType(dataTable) {
|
|
|
117
114
|
}
|
|
118
115
|
|
|
119
116
|
function maskSecretInTitle(scenarios) {
|
|
120
|
-
scenarios.forEach(
|
|
117
|
+
scenarios.forEach(scenario => {
|
|
121
118
|
const res = []
|
|
122
119
|
|
|
123
|
-
scenario.test.title.split(',').forEach(
|
|
120
|
+
scenario.test.title.split(',').forEach(item => {
|
|
124
121
|
res.push(item.replace(/{"_secret":"(.*)"}/, '"*****"'))
|
|
125
122
|
})
|
|
126
123
|
|
|
@@ -10,7 +10,7 @@ class DataScenarioConfig {
|
|
|
10
10
|
* @param {*} err
|
|
11
11
|
*/
|
|
12
12
|
throws(err) {
|
|
13
|
-
this.scenarios.forEach(
|
|
13
|
+
this.scenarios.forEach(scenario => scenario.throws(err))
|
|
14
14
|
return this
|
|
15
15
|
}
|
|
16
16
|
|
|
@@ -21,7 +21,7 @@ class DataScenarioConfig {
|
|
|
21
21
|
*
|
|
22
22
|
*/
|
|
23
23
|
fails() {
|
|
24
|
-
this.scenarios.forEach(
|
|
24
|
+
this.scenarios.forEach(scenario => scenario.fails())
|
|
25
25
|
return this
|
|
26
26
|
}
|
|
27
27
|
|
|
@@ -31,7 +31,7 @@ class DataScenarioConfig {
|
|
|
31
31
|
* @param {*} retries
|
|
32
32
|
*/
|
|
33
33
|
retry(retries) {
|
|
34
|
-
this.scenarios.forEach(
|
|
34
|
+
this.scenarios.forEach(scenario => scenario.retry(retries))
|
|
35
35
|
return this
|
|
36
36
|
}
|
|
37
37
|
|
|
@@ -40,7 +40,7 @@ class DataScenarioConfig {
|
|
|
40
40
|
* @param {*} timeout
|
|
41
41
|
*/
|
|
42
42
|
timeout(timeout) {
|
|
43
|
-
this.scenarios.forEach(
|
|
43
|
+
this.scenarios.forEach(scenario => scenario.timeout(timeout))
|
|
44
44
|
return this
|
|
45
45
|
}
|
|
46
46
|
|
|
@@ -49,7 +49,7 @@ class DataScenarioConfig {
|
|
|
49
49
|
* Helper name can be omitted and values will be applied to first helper.
|
|
50
50
|
*/
|
|
51
51
|
config(helper, obj) {
|
|
52
|
-
this.scenarios.forEach(
|
|
52
|
+
this.scenarios.forEach(scenario => scenario.config(helper, obj))
|
|
53
53
|
return this
|
|
54
54
|
}
|
|
55
55
|
|
|
@@ -58,7 +58,7 @@ class DataScenarioConfig {
|
|
|
58
58
|
* @param {*} tagName
|
|
59
59
|
*/
|
|
60
60
|
tag(tagName) {
|
|
61
|
-
this.scenarios.forEach(
|
|
61
|
+
this.scenarios.forEach(scenario => scenario.tag(tagName))
|
|
62
62
|
return this
|
|
63
63
|
}
|
|
64
64
|
|
|
@@ -67,7 +67,7 @@ class DataScenarioConfig {
|
|
|
67
67
|
* @param {*} obj
|
|
68
68
|
*/
|
|
69
69
|
inject(obj) {
|
|
70
|
-
this.scenarios.forEach(
|
|
70
|
+
this.scenarios.forEach(scenario => scenario.inject(obj))
|
|
71
71
|
return this
|
|
72
72
|
}
|
|
73
73
|
|
|
@@ -76,7 +76,7 @@ class DataScenarioConfig {
|
|
|
76
76
|
* @param {*} dependencies
|
|
77
77
|
*/
|
|
78
78
|
injectDependencies(dependencies) {
|
|
79
|
-
this.scenarios.forEach(
|
|
79
|
+
this.scenarios.forEach(scenario => scenario.injectDependencies(dependencies))
|
|
80
80
|
return this
|
|
81
81
|
}
|
|
82
82
|
}
|
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
class DataTableArgument {
|
|
6
6
|
/** @param {*} gherkinDataTable */
|
|
7
7
|
constructor(gherkinDataTable) {
|
|
8
|
-
this.rawData = gherkinDataTable.rows.map(
|
|
9
|
-
return row.cells.map(
|
|
8
|
+
this.rawData = gherkinDataTable.rows.map(row => {
|
|
9
|
+
return row.cells.map(cell => {
|
|
10
10
|
return cell.value
|
|
11
11
|
})
|
|
12
12
|
})
|
|
@@ -34,7 +34,7 @@ class DataTableArgument {
|
|
|
34
34
|
hashes() {
|
|
35
35
|
const copy = this.raw()
|
|
36
36
|
const header = copy.shift()
|
|
37
|
-
return copy.map(
|
|
37
|
+
return copy.map(row => {
|
|
38
38
|
const r = {}
|
|
39
39
|
row.forEach((cell, index) => (r[header[index]] = cell))
|
|
40
40
|
return r
|
|
@@ -47,19 +47,19 @@ class DataTableArgument {
|
|
|
47
47
|
*/
|
|
48
48
|
rowsHash() {
|
|
49
49
|
const rows = this.raw()
|
|
50
|
-
const everyRowHasTwoColumns = rows.every(
|
|
50
|
+
const everyRowHasTwoColumns = rows.every(row => row.length === 2)
|
|
51
51
|
if (!everyRowHasTwoColumns) {
|
|
52
52
|
throw new Error('rowsHash can only be called on a data table where all rows have exactly two columns')
|
|
53
53
|
}
|
|
54
54
|
/** @type {Record<string, string>} */
|
|
55
55
|
const result = {}
|
|
56
|
-
rows.forEach(
|
|
56
|
+
rows.forEach(x => (result[x[0]] = x[1]))
|
|
57
57
|
return result
|
|
58
58
|
}
|
|
59
59
|
|
|
60
60
|
/** Transposed the data */
|
|
61
61
|
transpose() {
|
|
62
|
-
this.rawData = this.rawData[0].map((x, i) => this.rawData.map(
|
|
62
|
+
this.rawData = this.rawData[0].map((x, i) => this.rawData.map(y => y[i]))
|
|
63
63
|
}
|
|
64
64
|
}
|
|
65
65
|
|
package/lib/data/table.js
CHANGED
|
@@ -10,13 +10,10 @@ class DataTable {
|
|
|
10
10
|
|
|
11
11
|
/** @param {Array<*>} array */
|
|
12
12
|
add(array) {
|
|
13
|
-
if (array.length !== this.array.length)
|
|
14
|
-
throw new Error(
|
|
15
|
-
`There is too many elements in given data array. Please provide data in this format: ${this.array}`,
|
|
16
|
-
)
|
|
13
|
+
if (array.length !== this.array.length) throw new Error(`There is too many elements in given data array. Please provide data in this format: ${this.array}`)
|
|
17
14
|
const tempObj = {}
|
|
18
15
|
let arrayCounter = 0
|
|
19
|
-
this.array.forEach(
|
|
16
|
+
this.array.forEach(elem => {
|
|
20
17
|
tempObj[elem] = array[arrayCounter]
|
|
21
18
|
tempObj.toString = () => JSON.stringify(tempObj)
|
|
22
19
|
arrayCounter++
|
|
@@ -26,13 +23,10 @@ class DataTable {
|
|
|
26
23
|
|
|
27
24
|
/** @param {Array<*>} array */
|
|
28
25
|
xadd(array) {
|
|
29
|
-
if (array.length !== this.array.length)
|
|
30
|
-
throw new Error(
|
|
31
|
-
`There is too many elements in given data array. Please provide data in this format: ${this.array}`,
|
|
32
|
-
)
|
|
26
|
+
if (array.length !== this.array.length) throw new Error(`There is too many elements in given data array. Please provide data in this format: ${this.array}`)
|
|
33
27
|
const tempObj = {}
|
|
34
28
|
let arrayCounter = 0
|
|
35
|
-
this.array.forEach(
|
|
29
|
+
this.array.forEach(elem => {
|
|
36
30
|
tempObj[elem] = array[arrayCounter]
|
|
37
31
|
tempObj.toString = () => JSON.stringify(tempObj)
|
|
38
32
|
arrayCounter++
|
|
@@ -42,7 +36,7 @@ class DataTable {
|
|
|
42
36
|
|
|
43
37
|
/** @param {Function} func */
|
|
44
38
|
filter(func) {
|
|
45
|
-
return this.rows.filter(
|
|
39
|
+
return this.rows.filter(row => func(row.data))
|
|
46
40
|
}
|
|
47
41
|
}
|
|
48
42
|
|
package/lib/els.js
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
const output = require('./output');
|
|
2
|
+
const store = require('./store');
|
|
3
|
+
const recorder = require('./recorder');
|
|
4
|
+
const container = require('./container');
|
|
5
|
+
const event = require('./event');
|
|
6
|
+
const Step = require('./step');
|
|
7
|
+
const { truth } = require('./assert/truth');
|
|
8
|
+
const { isAsyncFunction, humanizeFunction } = require('./utils');
|
|
9
|
+
|
|
10
|
+
function element(purpose, locator, fn) {
|
|
11
|
+
if (!fn) {
|
|
12
|
+
fn = locator;
|
|
13
|
+
locator = purpose;
|
|
14
|
+
purpose = 'first element';
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const step = prepareStep(purpose, locator, fn);
|
|
18
|
+
if (!step) return;
|
|
19
|
+
|
|
20
|
+
return executeStep(step, async () => {
|
|
21
|
+
const els = await step.helper._locate(locator);
|
|
22
|
+
output.debug(`Found ${els.length} elements, using first element`);
|
|
23
|
+
|
|
24
|
+
return fn(els[0]);
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function eachElement(purpose, locator, fn) {
|
|
29
|
+
if (!fn) {
|
|
30
|
+
fn = locator;
|
|
31
|
+
locator = purpose;
|
|
32
|
+
purpose = 'for each element';
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const step = prepareStep(purpose, locator, fn);
|
|
36
|
+
if (!step) return;
|
|
37
|
+
|
|
38
|
+
return executeStep(step, async () => {
|
|
39
|
+
const els = await step.helper._locate(locator);
|
|
40
|
+
output.debug(`Found ${els.length} elements for each elements to iterate`);
|
|
41
|
+
|
|
42
|
+
const errs = [];
|
|
43
|
+
let i = 0;
|
|
44
|
+
for (const el of els) {
|
|
45
|
+
try {
|
|
46
|
+
await fn(el, i);
|
|
47
|
+
} catch (err) {
|
|
48
|
+
output.error(`eachElement: failed operation on element #${i} ${el}`);
|
|
49
|
+
errs.push(err);
|
|
50
|
+
}
|
|
51
|
+
i++;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (errs.length) {
|
|
55
|
+
throw errs[0];
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function expectElement(locator, fn) {
|
|
61
|
+
const step = prepareStep('expect element to be', locator, fn);
|
|
62
|
+
if (!step) return;
|
|
63
|
+
|
|
64
|
+
return executeStep(step, async () => {
|
|
65
|
+
const els = await step.helper._locate(locator);
|
|
66
|
+
output.debug(`Found ${els.length} elements, first will be used for assertion`);
|
|
67
|
+
|
|
68
|
+
const result = await fn(els[0]);
|
|
69
|
+
const assertion = truth(`element (${locator})`, fn.toString());
|
|
70
|
+
assertion.assert(result);
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function expectAnyElement(locator, fn) {
|
|
75
|
+
const step = prepareStep('expect any element to be', locator, fn);
|
|
76
|
+
if (!step) return;
|
|
77
|
+
|
|
78
|
+
return executeStep(step, async () => {
|
|
79
|
+
const els = await step.helper._locate(locator);
|
|
80
|
+
output.debug(`Found ${els.length} elements, at least one should pass the assertion`);
|
|
81
|
+
|
|
82
|
+
const assertion = truth(`any element of (${locator})`, fn.toString());
|
|
83
|
+
|
|
84
|
+
let found = false;
|
|
85
|
+
for (const el of els) {
|
|
86
|
+
const result = await fn(el);
|
|
87
|
+
if (result) {
|
|
88
|
+
found = true;
|
|
89
|
+
break;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
if (!found) throw assertion.getException();
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function expectAllElements(locator, fn) {
|
|
97
|
+
const step = prepareStep('expect all elements', locator, fn);
|
|
98
|
+
if (!step) return;
|
|
99
|
+
|
|
100
|
+
return executeStep(step, async () => {
|
|
101
|
+
const els = await step.helper._locate(locator);
|
|
102
|
+
output.debug(`Found ${els.length} elements, all should pass the assertion`);
|
|
103
|
+
|
|
104
|
+
let i = 1;
|
|
105
|
+
for (const el of els) {
|
|
106
|
+
output.debug(`checking element #${i}: ${el}`);
|
|
107
|
+
const result = await fn(el);
|
|
108
|
+
const assertion = truth(`element #${i} of (${locator})`, humanizeFunction(fn));
|
|
109
|
+
assertion.assert(result);
|
|
110
|
+
i++;
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
module.exports = {
|
|
116
|
+
element,
|
|
117
|
+
eachElement,
|
|
118
|
+
expectElement,
|
|
119
|
+
expectAnyElement,
|
|
120
|
+
expectAllElements,
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
function prepareStep(purpose, locator, fn) {
|
|
124
|
+
if (store.dryRun) return;
|
|
125
|
+
const helpers = Object.values(container.helpers());
|
|
126
|
+
|
|
127
|
+
const helper = helpers.filter(h => !!h._locate)[0];
|
|
128
|
+
|
|
129
|
+
if (!helper) {
|
|
130
|
+
throw new Error('No helper enabled with _locate method with returns a list of elements.');
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (!isAsyncFunction(fn)) {
|
|
134
|
+
throw new Error('Async function should be passed into each element');
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const isAssertion = purpose.startsWith('expect');
|
|
138
|
+
|
|
139
|
+
const step = new Step(helper, `${purpose} within "${locator}" ${isAssertion ? 'to be' : 'to'}`);
|
|
140
|
+
step.setActor('EL');
|
|
141
|
+
step.setArguments([humanizeFunction(fn)]);
|
|
142
|
+
step.helperMethod = '_locate';
|
|
143
|
+
|
|
144
|
+
return step;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
async function executeStep(step, action) {
|
|
148
|
+
let error;
|
|
149
|
+
const promise = recorder.add('register element wrapper', async () => {
|
|
150
|
+
event.emit(event.step.started, step);
|
|
151
|
+
|
|
152
|
+
try {
|
|
153
|
+
await action();
|
|
154
|
+
} catch (err) {
|
|
155
|
+
recorder.throw(err);
|
|
156
|
+
event.emit(event.step.failed, step, err);
|
|
157
|
+
event.emit(event.step.finished, step);
|
|
158
|
+
// event.emit(event.step.after, step)
|
|
159
|
+
error = err;
|
|
160
|
+
// await recorder.promise();
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
event.emit(event.step.after, step);
|
|
165
|
+
event.emit(event.step.passed, step);
|
|
166
|
+
event.emit(event.step.finished, step);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
// await recorder.promise();
|
|
170
|
+
|
|
171
|
+
// if (error) {
|
|
172
|
+
// console.log('error', error.inspect())
|
|
173
|
+
// return recorder.throw(error);
|
|
174
|
+
// }
|
|
175
|
+
|
|
176
|
+
return promise;
|
|
177
|
+
}
|
package/lib/event.js
CHANGED
package/lib/heal.js
CHANGED
|
@@ -1,122 +1,125 @@
|
|
|
1
|
-
const debug = require('debug')('codeceptjs:heal')
|
|
2
|
-
const colors = require('chalk')
|
|
3
|
-
const Container = require('./container')
|
|
4
|
-
const recorder = require('./recorder')
|
|
5
|
-
const output = require('./output')
|
|
6
|
-
const event = require('./event')
|
|
1
|
+
const debug = require('debug')('codeceptjs:heal')
|
|
2
|
+
const colors = require('chalk')
|
|
3
|
+
const Container = require('./container')
|
|
4
|
+
const recorder = require('./recorder')
|
|
5
|
+
const output = require('./output')
|
|
6
|
+
const event = require('./event')
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* @class
|
|
10
10
|
*/
|
|
11
11
|
class Heal {
|
|
12
12
|
constructor() {
|
|
13
|
-
this.recipes = {}
|
|
14
|
-
this.fixes = []
|
|
15
|
-
this.prepareFns = []
|
|
16
|
-
this.contextName = null
|
|
17
|
-
this.numHealed = 0
|
|
13
|
+
this.recipes = {}
|
|
14
|
+
this.fixes = []
|
|
15
|
+
this.prepareFns = []
|
|
16
|
+
this.contextName = null
|
|
17
|
+
this.numHealed = 0
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
clear() {
|
|
21
|
-
this.recipes = {}
|
|
22
|
-
this.fixes = []
|
|
23
|
-
this.prepareFns = []
|
|
24
|
-
this.contextName = null
|
|
25
|
-
this.numHealed = 0
|
|
21
|
+
this.recipes = {}
|
|
22
|
+
this.fixes = []
|
|
23
|
+
this.prepareFns = []
|
|
24
|
+
this.contextName = null
|
|
25
|
+
this.numHealed = 0
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
addRecipe(name, opts = {}) {
|
|
29
|
-
if (!opts.priority) opts.priority = 0
|
|
29
|
+
if (!opts.priority) opts.priority = 0
|
|
30
30
|
|
|
31
|
-
if (!opts.fn) throw new Error(`Recipe ${name} should have a function 'fn' to execute`)
|
|
31
|
+
if (!opts.fn) throw new Error(`Recipe ${name} should have a function 'fn' to execute`)
|
|
32
32
|
|
|
33
|
-
this.recipes[name] = opts
|
|
33
|
+
this.recipes[name] = opts
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
connectToEvents() {
|
|
37
|
-
event.dispatcher.on(event.suite.before,
|
|
38
|
-
this.contextName = suite.title
|
|
39
|
-
})
|
|
37
|
+
event.dispatcher.on(event.suite.before, suite => {
|
|
38
|
+
this.contextName = suite.title
|
|
39
|
+
})
|
|
40
40
|
|
|
41
|
-
event.dispatcher.on(event.test.started,
|
|
42
|
-
this.contextName = test.fullTitle()
|
|
43
|
-
})
|
|
41
|
+
event.dispatcher.on(event.test.started, test => {
|
|
42
|
+
this.contextName = test.fullTitle()
|
|
43
|
+
})
|
|
44
44
|
|
|
45
45
|
event.dispatcher.on(event.test.finished, () => {
|
|
46
|
-
this.contextName = null
|
|
47
|
-
})
|
|
46
|
+
this.contextName = null
|
|
47
|
+
})
|
|
48
48
|
}
|
|
49
49
|
|
|
50
50
|
hasCorrespondingRecipes(step) {
|
|
51
|
-
return matchRecipes(this.recipes, this.contextName)
|
|
52
|
-
.filter(r => !r.steps || r.steps.includes(step.name))
|
|
53
|
-
.length > 0;
|
|
51
|
+
return matchRecipes(this.recipes, this.contextName).filter(r => !r.steps || r.steps.includes(step.name)).length > 0
|
|
54
52
|
}
|
|
55
53
|
|
|
56
54
|
async getCodeSuggestions(context) {
|
|
57
|
-
const suggestions = []
|
|
58
|
-
const recipes = matchRecipes(this.recipes, this.contextName)
|
|
55
|
+
const suggestions = []
|
|
56
|
+
const recipes = matchRecipes(this.recipes, this.contextName)
|
|
59
57
|
|
|
60
|
-
debug('Recipes', recipes)
|
|
58
|
+
debug('Recipes', recipes)
|
|
61
59
|
|
|
62
|
-
const currentOutputLevel = output.level()
|
|
63
|
-
output.level(0)
|
|
60
|
+
const currentOutputLevel = output.level()
|
|
61
|
+
output.level(0)
|
|
64
62
|
|
|
65
|
-
for (const [property, prepareFn] of Object.entries(
|
|
66
|
-
|
|
63
|
+
for (const [property, prepareFn] of Object.entries(
|
|
64
|
+
recipes
|
|
65
|
+
.map(r => r.prepare)
|
|
66
|
+
.filter(p => !!p)
|
|
67
|
+
.reduce((acc, obj) => ({ ...acc, ...obj }), {}),
|
|
68
|
+
)) {
|
|
69
|
+
if (!prepareFn) continue
|
|
67
70
|
|
|
68
|
-
if (context[property]) continue
|
|
69
|
-
context[property] = await prepareFn(Container.support())
|
|
71
|
+
if (context[property]) continue
|
|
72
|
+
context[property] = await prepareFn(Container.support())
|
|
70
73
|
}
|
|
71
74
|
|
|
72
|
-
output.level(currentOutputLevel)
|
|
75
|
+
output.level(currentOutputLevel)
|
|
73
76
|
|
|
74
77
|
for (const recipe of recipes) {
|
|
75
|
-
let snippets = await recipe.fn(context)
|
|
76
|
-
if (!Array.isArray(snippets)) snippets = [snippets]
|
|
78
|
+
let snippets = await recipe.fn(context)
|
|
79
|
+
if (!Array.isArray(snippets)) snippets = [snippets]
|
|
77
80
|
|
|
78
81
|
suggestions.push({
|
|
79
82
|
name: recipe.name,
|
|
80
83
|
snippets,
|
|
81
|
-
})
|
|
84
|
+
})
|
|
82
85
|
}
|
|
83
86
|
|
|
84
|
-
return suggestions.filter(s => !isBlank(s.snippets))
|
|
87
|
+
return suggestions.filter(s => !isBlank(s.snippets))
|
|
85
88
|
}
|
|
86
89
|
|
|
87
90
|
async healStep(failedStep, error, failureContext = {}) {
|
|
88
|
-
output.debug(`Trying to heal ${failedStep.toCode()} step`)
|
|
91
|
+
output.debug(`Trying to heal ${failedStep.toCode()} step`)
|
|
89
92
|
|
|
90
93
|
Object.assign(failureContext, {
|
|
91
94
|
error,
|
|
92
95
|
step: failedStep,
|
|
93
96
|
prevSteps: failureContext?.test?.steps?.slice(0, -1) || [],
|
|
94
|
-
})
|
|
97
|
+
})
|
|
95
98
|
|
|
96
|
-
const suggestions = await this.getCodeSuggestions(failureContext)
|
|
99
|
+
const suggestions = await this.getCodeSuggestions(failureContext)
|
|
97
100
|
|
|
98
101
|
if (suggestions.length === 0) {
|
|
99
|
-
debug('No healing suggestions found')
|
|
100
|
-
throw error
|
|
102
|
+
debug('No healing suggestions found')
|
|
103
|
+
throw error
|
|
101
104
|
}
|
|
102
105
|
|
|
103
|
-
output.debug(`Received ${suggestions.length} suggestion${suggestions.length === 1 ? '' : 's'}`)
|
|
106
|
+
output.debug(`Received ${suggestions.length} suggestion${suggestions.length === 1 ? '' : 's'}`)
|
|
104
107
|
|
|
105
|
-
debug(suggestions)
|
|
108
|
+
debug(suggestions)
|
|
106
109
|
|
|
107
110
|
for (const suggestion of suggestions) {
|
|
108
111
|
for (const codeSnippet of suggestion.snippets) {
|
|
109
112
|
try {
|
|
110
|
-
debug('Executing', codeSnippet)
|
|
111
|
-
recorder.catch(
|
|
112
|
-
debug(e)
|
|
113
|
-
})
|
|
113
|
+
debug('Executing', codeSnippet)
|
|
114
|
+
recorder.catch(e => {
|
|
115
|
+
debug(e)
|
|
116
|
+
})
|
|
114
117
|
|
|
115
118
|
if (typeof codeSnippet === 'string') {
|
|
116
|
-
const I = Container.support('I')
|
|
117
|
-
await eval(codeSnippet)
|
|
119
|
+
const I = Container.support('I')
|
|
120
|
+
await eval(codeSnippet)
|
|
118
121
|
} else if (typeof codeSnippet === 'function') {
|
|
119
|
-
await codeSnippet(Container.support())
|
|
122
|
+
await codeSnippet(Container.support())
|
|
120
123
|
}
|
|
121
124
|
|
|
122
125
|
this.fixes.push({
|
|
@@ -124,49 +127,44 @@ class Heal {
|
|
|
124
127
|
test: failureContext?.test,
|
|
125
128
|
step: failedStep,
|
|
126
129
|
snippet: codeSnippet,
|
|
127
|
-
})
|
|
130
|
+
})
|
|
128
131
|
|
|
129
|
-
recorder.add('healed', () => output.print(colors.bold.green(` Code healed successfully by ${suggestion.name}`), colors.gray('(no errors thrown)')))
|
|
130
|
-
this.numHealed
|
|
132
|
+
recorder.add('healed', () => output.print(colors.bold.green(` Code healed successfully by ${suggestion.name}`), colors.gray('(no errors thrown)')))
|
|
133
|
+
this.numHealed++
|
|
131
134
|
// recorder.session.restore();
|
|
132
|
-
return
|
|
135
|
+
return
|
|
133
136
|
} catch (err) {
|
|
134
|
-
debug('Failed to execute code', err)
|
|
135
|
-
recorder.ignoreErr(err)
|
|
136
|
-
recorder.catchWithoutStop(err)
|
|
137
|
-
await recorder.promise()
|
|
137
|
+
debug('Failed to execute code', err)
|
|
138
|
+
recorder.ignoreErr(err) // healing did not help
|
|
139
|
+
recorder.catchWithoutStop(err)
|
|
140
|
+
await recorder.promise() // wait for all promises to resolve
|
|
138
141
|
}
|
|
139
142
|
}
|
|
140
143
|
}
|
|
141
|
-
output.debug(`Couldn't heal the code for ${failedStep.toCode()}`)
|
|
142
|
-
recorder.throw(error)
|
|
144
|
+
output.debug(`Couldn't heal the code for ${failedStep.toCode()}`)
|
|
145
|
+
recorder.throw(error)
|
|
143
146
|
}
|
|
144
147
|
|
|
145
148
|
static setDefaultHealers() {
|
|
146
|
-
require('./template/heal')
|
|
149
|
+
require('./template/heal')
|
|
147
150
|
}
|
|
148
151
|
}
|
|
149
152
|
|
|
150
|
-
const heal = new Heal()
|
|
153
|
+
const heal = new Heal()
|
|
151
154
|
|
|
152
|
-
module.exports = heal
|
|
155
|
+
module.exports = heal
|
|
153
156
|
|
|
154
157
|
function matchRecipes(recipes, contextName) {
|
|
155
158
|
return Object.entries(recipes)
|
|
156
159
|
.filter(([, recipe]) => !contextName || !recipe.grep || new RegExp(recipe.grep).test(contextName))
|
|
157
160
|
.sort(([, a], [, b]) => a.priority - b.priority)
|
|
158
161
|
.map(([name, recipe]) => {
|
|
159
|
-
recipe.name = name
|
|
160
|
-
return recipe
|
|
162
|
+
recipe.name = name
|
|
163
|
+
return recipe
|
|
161
164
|
})
|
|
162
|
-
.filter(r => !!r.fn)
|
|
165
|
+
.filter(r => !!r.fn)
|
|
163
166
|
}
|
|
164
167
|
|
|
165
168
|
function isBlank(value) {
|
|
166
|
-
return (
|
|
167
|
-
value == null
|
|
168
|
-
|| (Array.isArray(value) && value.length === 0)
|
|
169
|
-
|| (typeof value === 'object' && Object.keys(value).length === 0)
|
|
170
|
-
|| (typeof value === 'string' && value.trim() === '')
|
|
171
|
-
);
|
|
169
|
+
return value == null || (Array.isArray(value) && value.length === 0) || (typeof value === 'object' && Object.keys(value).length === 0) || (typeof value === 'string' && value.trim() === '')
|
|
172
170
|
}
|
|
@@ -217,7 +217,7 @@ class ApiDataFactory extends Helper {
|
|
|
217
217
|
}
|
|
218
218
|
|
|
219
219
|
this.created = {}
|
|
220
|
-
Object.keys(this.factories).forEach(
|
|
220
|
+
Object.keys(this.factories).forEach(f => (this.created[f] = []))
|
|
221
221
|
}
|
|
222
222
|
|
|
223
223
|
static _checkRequirements() {
|
|
@@ -357,7 +357,7 @@ Current file error: ${err.message}`)
|
|
|
357
357
|
|
|
358
358
|
request.baseURL = this.config.endpoint
|
|
359
359
|
|
|
360
|
-
return this.restHelper._executeRequest(request).then(
|
|
360
|
+
return this.restHelper._executeRequest(request).then(resp => {
|
|
361
361
|
const id = this._fetchId(resp.data, factory)
|
|
362
362
|
this.created[factory].push(id)
|
|
363
363
|
this.debugSection('Created', `Id: ${id}`)
|
|
@@ -391,10 +391,7 @@ Current file error: ${err.message}`)
|
|
|
391
391
|
request.baseURL = this.config.endpoint
|
|
392
392
|
|
|
393
393
|
if (request.url.match(/^undefined/)) {
|
|
394
|
-
return this.debugSection(
|
|
395
|
-
'Please configure the delete request in your ApiDataFactory helper',
|
|
396
|
-
"delete: () => ({ method: 'DELETE', url: '/api/users' })",
|
|
397
|
-
)
|
|
394
|
+
return this.debugSection('Please configure the delete request in your ApiDataFactory helper', "delete: () => ({ method: 'DELETE', url: '/api/users' })")
|
|
398
395
|
}
|
|
399
396
|
|
|
400
397
|
return this.restHelper._executeRequest(request).then(() => {
|