codeceptjs 3.1.1 → 3.2.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 +120 -0
- package/README.md +2 -3
- package/bin/codecept.js +1 -0
- package/docs/advanced.md +94 -60
- package/docs/basics.md +1 -1
- package/docs/bdd.md +55 -1
- package/docs/build/Appium.js +22 -4
- package/docs/build/FileSystem.js +1 -0
- package/docs/build/Playwright.js +40 -42
- package/docs/build/Protractor.js +9 -24
- package/docs/build/Puppeteer.js +28 -30
- package/docs/build/REST.js +1 -0
- package/docs/build/WebDriver.js +2 -24
- package/docs/changelog.md +120 -0
- package/docs/commands.md +21 -7
- package/docs/configuration.md +15 -2
- package/docs/custom-helpers.md +1 -36
- package/docs/helpers/Appium.md +49 -50
- package/docs/helpers/FileSystem.md +1 -1
- package/docs/helpers/Playwright.md +16 -18
- package/docs/helpers/Puppeteer.md +18 -18
- package/docs/helpers/REST.md +3 -1
- package/docs/helpers/WebDriver.md +1 -17
- package/docs/mobile-react-native-locators.md +3 -0
- package/docs/playwright.md +40 -0
- package/docs/plugins.md +187 -70
- package/docs/reports.md +23 -5
- package/lib/actor.js +20 -2
- package/lib/codecept.js +15 -2
- package/lib/command/info.js +1 -1
- package/lib/config.js +13 -1
- package/lib/container.js +3 -1
- package/lib/data/dataTableArgument.js +35 -0
- package/lib/helper/Appium.js +22 -4
- package/lib/helper/FileSystem.js +1 -0
- package/lib/helper/Playwright.js +40 -32
- package/lib/helper/Protractor.js +2 -14
- package/lib/helper/Puppeteer.js +21 -20
- package/lib/helper/REST.js +1 -0
- package/lib/helper/WebDriver.js +2 -14
- package/lib/index.js +2 -0
- package/lib/interfaces/featureConfig.js +3 -0
- package/lib/interfaces/gherkin.js +7 -1
- package/lib/interfaces/scenarioConfig.js +4 -0
- package/lib/listener/helpers.js +1 -0
- package/lib/listener/steps.js +21 -3
- package/lib/listener/timeout.js +71 -0
- package/lib/locator.js +3 -0
- package/lib/mochaFactory.js +13 -9
- package/lib/plugin/allure.js +6 -1
- package/lib/plugin/{puppeteerCoverage.js → coverage.js} +10 -22
- package/lib/plugin/customLocator.js +2 -2
- package/lib/plugin/retryFailedStep.js +4 -3
- package/lib/plugin/retryTo.js +130 -0
- package/lib/plugin/screenshotOnFail.js +1 -0
- package/lib/plugin/stepByStepReport.js +7 -0
- package/lib/plugin/stepTimeout.js +90 -0
- package/lib/plugin/subtitles.js +88 -0
- package/lib/plugin/tryTo.js +1 -1
- package/lib/recorder.js +21 -8
- package/lib/step.js +7 -2
- package/lib/store.js +2 -0
- package/lib/ui.js +2 -2
- package/package.json +6 -7
- package/typings/index.d.ts +8 -1
- package/typings/types.d.ts +104 -71
- package/docs/angular.md +0 -325
- package/docs/helpers/Protractor.md +0 -1658
- package/docs/webapi/waitUntil.mustache +0 -11
- package/typings/Protractor.d.ts +0 -16
package/lib/mochaFactory.js
CHANGED
|
@@ -2,7 +2,7 @@ const Mocha = require('mocha');
|
|
|
2
2
|
const fsPath = require('path');
|
|
3
3
|
const fs = require('fs');
|
|
4
4
|
const reporter = require('./cli');
|
|
5
|
-
const gherkinParser = require('./interfaces/gherkin
|
|
5
|
+
const gherkinParser = require('./interfaces/gherkin');
|
|
6
6
|
const output = require('./output');
|
|
7
7
|
const { genTestId } = require('./utils');
|
|
8
8
|
const ConnectionRefused = require('./helper/errors/ConnectionRefused');
|
|
@@ -17,12 +17,6 @@ class MochaFactory {
|
|
|
17
17
|
output.process(opts.child);
|
|
18
18
|
mocha.ui(scenarioUi);
|
|
19
19
|
|
|
20
|
-
// process.on('unhandledRejection', (reason) => {
|
|
21
|
-
// output.error('Unhandled rejection');
|
|
22
|
-
// console.log(Error.captureStackTrace(reason));
|
|
23
|
-
// output.error(reason);
|
|
24
|
-
// });
|
|
25
|
-
|
|
26
20
|
Mocha.Runner.prototype.uncaught = function (err) {
|
|
27
21
|
if (err) {
|
|
28
22
|
if (err.toString().indexOf('ECONNREFUSED') >= 0) {
|
|
@@ -41,8 +35,7 @@ class MochaFactory {
|
|
|
41
35
|
if (mocha.suite.suites.length === 0) {
|
|
42
36
|
mocha.files
|
|
43
37
|
.filter(file => file.match(/\.feature$/))
|
|
44
|
-
.
|
|
45
|
-
.forEach(content => mocha.suite.addSuite(gherkinParser(content)));
|
|
38
|
+
.forEach(file => mocha.suite.addSuite(gherkinParser(fs.readFileSync(file, 'utf8'), file)));
|
|
46
39
|
|
|
47
40
|
// remove feature files
|
|
48
41
|
mocha.files = mocha.files.filter(file => !file.match(/\.feature$/));
|
|
@@ -51,19 +44,30 @@ class MochaFactory {
|
|
|
51
44
|
|
|
52
45
|
// add ids for each test and check uniqueness
|
|
53
46
|
const dupes = [];
|
|
47
|
+
let missingFeatureInFile = [];
|
|
54
48
|
const seenTests = [];
|
|
55
49
|
mocha.suite.eachTest(test => {
|
|
56
50
|
test.id = genTestId(test);
|
|
51
|
+
|
|
57
52
|
const name = test.fullTitle();
|
|
58
53
|
if (seenTests.includes(test.id)) {
|
|
59
54
|
dupes.push(name);
|
|
60
55
|
}
|
|
61
56
|
seenTests.push(test.id);
|
|
57
|
+
|
|
58
|
+
if (name.slice(0, name.indexOf(':')) === '') {
|
|
59
|
+
missingFeatureInFile.push(test.file);
|
|
60
|
+
}
|
|
62
61
|
});
|
|
63
62
|
if (dupes.length) {
|
|
64
63
|
// ideally this should be no-op and throw (breaking change)...
|
|
65
64
|
output.error(`Duplicate test names detected - Feature + Scenario name should be unique:\n${dupes.join('\n')}`);
|
|
66
65
|
}
|
|
66
|
+
|
|
67
|
+
if (missingFeatureInFile.length) {
|
|
68
|
+
missingFeatureInFile = [...new Set(missingFeatureInFile)];
|
|
69
|
+
output.error(`Missing Feature section in:\n${missingFeatureInFile.join('\n')}`);
|
|
70
|
+
}
|
|
67
71
|
}
|
|
68
72
|
};
|
|
69
73
|
|
package/lib/plugin/allure.js
CHANGED
|
@@ -283,7 +283,12 @@ module.exports = (config) => {
|
|
|
283
283
|
let maxLevel;
|
|
284
284
|
function finishMetastep(level) {
|
|
285
285
|
const metaStepsToFinish = currentMetaStep.splice(maxLevel - level);
|
|
286
|
-
metaStepsToFinish.forEach(() =>
|
|
286
|
+
metaStepsToFinish.forEach(() => {
|
|
287
|
+
// only if the current step is of type Step, end it.
|
|
288
|
+
if (reporter.suites && reporter.suites.length && reporter.suites[0].currentStep && reporter.suites[0].currentStep.constructor.name === 'Step') {
|
|
289
|
+
reporter.endStep('passed');
|
|
290
|
+
}
|
|
291
|
+
});
|
|
287
292
|
}
|
|
288
293
|
|
|
289
294
|
function startMetaStep(metaStep, level = 0) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const
|
|
1
|
+
const debugModule = require('debug');
|
|
2
2
|
const fs = require('fs');
|
|
3
3
|
const path = require('path');
|
|
4
4
|
|
|
@@ -13,7 +13,7 @@ const defaultConfig = {
|
|
|
13
13
|
uniqueFileName: true,
|
|
14
14
|
};
|
|
15
15
|
|
|
16
|
-
const supportedHelpers = ['Puppeteer'];
|
|
16
|
+
const supportedHelpers = ['Puppeteer', 'Playwright'];
|
|
17
17
|
|
|
18
18
|
function buildFileName(test, uniqueFileName) {
|
|
19
19
|
let fileName = clearString(test.title);
|
|
@@ -41,15 +41,14 @@ function buildFileName(test, uniqueFileName) {
|
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
/**
|
|
44
|
-
* Dumps
|
|
44
|
+
* Dumps code coverage from Playwright/Puppeteer after every test.
|
|
45
45
|
*
|
|
46
46
|
* #### Configuration
|
|
47
47
|
*
|
|
48
|
-
* Configuration can either be taken from a corresponding helper (deprecated) or a from plugin config (recommended).
|
|
49
48
|
*
|
|
50
49
|
* ```js
|
|
51
50
|
* plugins: {
|
|
52
|
-
*
|
|
51
|
+
* coverage: {
|
|
53
52
|
* enabled: true
|
|
54
53
|
* }
|
|
55
54
|
* }
|
|
@@ -59,33 +58,22 @@ function buildFileName(test, uniqueFileName) {
|
|
|
59
58
|
*
|
|
60
59
|
* * `coverageDir`: directory to dump coverage files
|
|
61
60
|
* * `uniqueFileName`: generate a unique filename by adding uuid
|
|
62
|
-
*
|
|
63
|
-
* First of all, your mileage may vary!
|
|
64
|
-
*
|
|
65
|
-
* To work, you need the client javascript code to be NOT uglified. They need to be built in "development" mode.
|
|
66
|
-
* And the end of your tests, you'll get a directory full of coverage per test run. Now what?
|
|
67
|
-
* You'll need to convert the coverage code to something istanbul can read. Good news is someone wrote the code
|
|
68
|
-
* for you (see puppeteer-to-istanbul link below). Then using istanbul you need to combine the converted
|
|
69
|
-
* coverage and create a report. Good luck!
|
|
70
|
-
*
|
|
71
|
-
* Links:
|
|
72
|
-
* * https://github.com/GoogleChrome/puppeteer/blob/v1.12.2/docs/api.md#class-coverage
|
|
73
|
-
* * https://github.com/istanbuljs/puppeteer-to-istanbul
|
|
74
|
-
* * https://github.com/gotwarlost/istanbul
|
|
75
61
|
*/
|
|
76
62
|
module.exports = function (config) {
|
|
77
63
|
const helpers = Container.helpers();
|
|
78
64
|
let coverageRunning = false;
|
|
79
65
|
let helper;
|
|
80
66
|
|
|
67
|
+
let debug;
|
|
81
68
|
for (const helperName of supportedHelpers) {
|
|
82
69
|
if (Object.keys(helpers).indexOf(helperName) > -1) {
|
|
83
70
|
helper = helpers[helperName];
|
|
71
|
+
debug = debugModule(`codeceptjs:plugin:${helperName.toLowerCase()}Coverage`);
|
|
84
72
|
}
|
|
85
73
|
}
|
|
86
74
|
|
|
87
75
|
if (!helper) {
|
|
88
|
-
console.error('Coverage is only supported in Puppeteer');
|
|
76
|
+
console.error('Coverage is only supported in Puppeteer, Playwright');
|
|
89
77
|
return; // no helpers for screenshot
|
|
90
78
|
}
|
|
91
79
|
|
|
@@ -102,7 +90,7 @@ module.exports = function (config) {
|
|
|
102
90
|
'starting coverage',
|
|
103
91
|
async () => {
|
|
104
92
|
try {
|
|
105
|
-
if (!coverageRunning) {
|
|
93
|
+
if (!coverageRunning && helper.page && helper.page.coverage) {
|
|
106
94
|
debug('--> starting coverage <--');
|
|
107
95
|
coverageRunning = true;
|
|
108
96
|
await helper.page.coverage.startJSCoverage();
|
|
@@ -115,13 +103,13 @@ module.exports = function (config) {
|
|
|
115
103
|
);
|
|
116
104
|
});
|
|
117
105
|
|
|
118
|
-
// Save
|
|
106
|
+
// Save coverage data after every test run
|
|
119
107
|
event.dispatcher.on(event.test.after, async (test) => {
|
|
120
108
|
recorder.add(
|
|
121
109
|
'saving coverage',
|
|
122
110
|
async () => {
|
|
123
111
|
try {
|
|
124
|
-
if (coverageRunning) {
|
|
112
|
+
if (coverageRunning && helper.page && helper.page.coverage) {
|
|
125
113
|
debug('--> stopping coverage <--');
|
|
126
114
|
coverageRunning = false;
|
|
127
115
|
const coverage = await helper.page.coverage.stopJSCoverage();
|
|
@@ -38,7 +38,7 @@ const defaultConfig = {
|
|
|
38
38
|
* // in codecept.conf.js
|
|
39
39
|
* plugins: {
|
|
40
40
|
* customLocator: {
|
|
41
|
-
* enabled: true
|
|
41
|
+
* enabled: true,
|
|
42
42
|
* attribute: 'data-test'
|
|
43
43
|
* }
|
|
44
44
|
* }
|
|
@@ -57,7 +57,7 @@ const defaultConfig = {
|
|
|
57
57
|
* // in codecept.conf.js
|
|
58
58
|
* plugins: {
|
|
59
59
|
* customLocator: {
|
|
60
|
-
* enabled: true
|
|
60
|
+
* enabled: true,
|
|
61
61
|
* prefix: '=',
|
|
62
62
|
* attribute: 'data-qa'
|
|
63
63
|
* }
|
|
@@ -2,7 +2,7 @@ const event = require('../event');
|
|
|
2
2
|
const recorder = require('../recorder');
|
|
3
3
|
|
|
4
4
|
const defaultConfig = {
|
|
5
|
-
retries:
|
|
5
|
+
retries: 3,
|
|
6
6
|
defaultIgnoredSteps: [
|
|
7
7
|
'amOnPage',
|
|
8
8
|
'wait*',
|
|
@@ -11,6 +11,7 @@ const defaultConfig = {
|
|
|
11
11
|
'run*',
|
|
12
12
|
'have*',
|
|
13
13
|
],
|
|
14
|
+
factor: 1.5,
|
|
14
15
|
ignoredSteps: [],
|
|
15
16
|
};
|
|
16
17
|
|
|
@@ -36,9 +37,9 @@ const defaultConfig = {
|
|
|
36
37
|
*
|
|
37
38
|
* #### Configuration:
|
|
38
39
|
*
|
|
39
|
-
* * `retries` - number of retries (by default
|
|
40
|
+
* * `retries` - number of retries (by default 3),
|
|
40
41
|
* * `when` - function, when to perform a retry (accepts error as parameter)
|
|
41
|
-
* * `factor` - The exponential factor to use. Default is
|
|
42
|
+
* * `factor` - The exponential factor to use. Default is 1.5.
|
|
42
43
|
* * `minTimeout` - The number of milliseconds before starting the first retry. Default is 1000.
|
|
43
44
|
* * `maxTimeout` - The maximum number of milliseconds between two retries. Default is Infinity.
|
|
44
45
|
* * `randomize` - Randomizes the timeouts by multiplying with a factor between 1 to 2. Default is false.
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
const recorder = require('../recorder');
|
|
2
|
+
const store = require('../store');
|
|
3
|
+
const { debug } = require('../output');
|
|
4
|
+
|
|
5
|
+
const defaultConfig = {
|
|
6
|
+
registerGlobal: true,
|
|
7
|
+
pollInterval: 200,
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
*
|
|
12
|
+
*
|
|
13
|
+
* Adds global `retryTo` which retries steps a few times before failing.
|
|
14
|
+
*
|
|
15
|
+
* Enable this plugin in `codecept.conf.js` (enabled by default for new setups):
|
|
16
|
+
*
|
|
17
|
+
* ```js
|
|
18
|
+
* plugins: {
|
|
19
|
+
* retryTo: {
|
|
20
|
+
* enabled: true
|
|
21
|
+
* }
|
|
22
|
+
* }
|
|
23
|
+
* ```
|
|
24
|
+
*
|
|
25
|
+
* Use it in your tests:
|
|
26
|
+
*
|
|
27
|
+
* ```js
|
|
28
|
+
* // retry these steps 5 times before failing
|
|
29
|
+
* await retryTo((tryNum) => {
|
|
30
|
+
* I.switchTo('#editor frame');
|
|
31
|
+
* I.click('Open');
|
|
32
|
+
* I.see('Opened')
|
|
33
|
+
* }, 5);
|
|
34
|
+
* ```
|
|
35
|
+
* Set polling interval as 3rd argument (200ms by default):
|
|
36
|
+
*
|
|
37
|
+
* ```js
|
|
38
|
+
* // retry these steps 5 times before failing
|
|
39
|
+
* await retryTo((tryNum) => {
|
|
40
|
+
* I.switchTo('#editor frame');
|
|
41
|
+
* I.click('Open');
|
|
42
|
+
* I.see('Opened')
|
|
43
|
+
* }, 5, 100);
|
|
44
|
+
* ```
|
|
45
|
+
*
|
|
46
|
+
* Default polling interval can be changed in a config:
|
|
47
|
+
*
|
|
48
|
+
* ```js
|
|
49
|
+
* plugins: {
|
|
50
|
+
* retryTo: {
|
|
51
|
+
* enabled: true,
|
|
52
|
+
* pollInterval: 500,
|
|
53
|
+
* }
|
|
54
|
+
* }
|
|
55
|
+
* ```
|
|
56
|
+
*
|
|
57
|
+
* Disables retryFailedStep plugin for steps inside a block;
|
|
58
|
+
*
|
|
59
|
+
* Use this plugin if:
|
|
60
|
+
*
|
|
61
|
+
* * you need repeat a set of actions in flaky tests
|
|
62
|
+
* * iframe was not rendered and you need to retry switching to it
|
|
63
|
+
*
|
|
64
|
+
*
|
|
65
|
+
* #### Configuration
|
|
66
|
+
*
|
|
67
|
+
* * `pollInterval` - default interval between retries in ms. 200 by default.
|
|
68
|
+
* * `registerGlobal` - to register `retryTo` function globally, true by default
|
|
69
|
+
*
|
|
70
|
+
* If `registerGlobal` is false you can use retryTo from the plugin:
|
|
71
|
+
*
|
|
72
|
+
* ```js
|
|
73
|
+
* const retryTo = codeceptjs.container.plugins('retryTo');
|
|
74
|
+
* ```
|
|
75
|
+
*
|
|
76
|
+
*/
|
|
77
|
+
module.exports = function (config) {
|
|
78
|
+
config = Object.assign(defaultConfig, config);
|
|
79
|
+
|
|
80
|
+
if (config.registerGlobal) {
|
|
81
|
+
global.retryTo = retryTo;
|
|
82
|
+
}
|
|
83
|
+
return retryTo;
|
|
84
|
+
|
|
85
|
+
function retryTo(callback, maxTries, pollInterval = undefined) {
|
|
86
|
+
const mode = store.debugMode;
|
|
87
|
+
let tries = 1;
|
|
88
|
+
if (!pollInterval) pollInterval = config.pollInterval;
|
|
89
|
+
|
|
90
|
+
let err = null;
|
|
91
|
+
|
|
92
|
+
return new Promise((done) => {
|
|
93
|
+
const tryBlock = () => {
|
|
94
|
+
recorder.session.start(`retryTo ${tries}`);
|
|
95
|
+
callback(tries);
|
|
96
|
+
recorder.add(() => {
|
|
97
|
+
recorder.session.restore(`retryTo ${tries}`);
|
|
98
|
+
done(null);
|
|
99
|
+
});
|
|
100
|
+
recorder.session.catch((e) => {
|
|
101
|
+
err = e;
|
|
102
|
+
recorder.session.restore(`retryTo ${tries}`);
|
|
103
|
+
tries++;
|
|
104
|
+
// recorder.session.restore(`retryTo`);
|
|
105
|
+
if (tries <= maxTries) {
|
|
106
|
+
debug(`Error ${err}... Retrying`);
|
|
107
|
+
err = null;
|
|
108
|
+
|
|
109
|
+
recorder.add(`retryTo ${tries}`, () => {
|
|
110
|
+
tryBlock();
|
|
111
|
+
// recorder.add(() => new Promise(done => setTimeout(done, pollInterval)));
|
|
112
|
+
});
|
|
113
|
+
} else {
|
|
114
|
+
// recorder.throw(err);
|
|
115
|
+
done(null);
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
// return recorder.promise();
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
recorder.add('retryTo', async () => {
|
|
122
|
+
store.debugMode = true;
|
|
123
|
+
tryBlock();
|
|
124
|
+
// recorder.add(() => recorder.session.restore(`retryTo ${tries-1}`));
|
|
125
|
+
});
|
|
126
|
+
}).then(() => {
|
|
127
|
+
if (err) recorder.throw(err);
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
};
|
|
@@ -98,6 +98,7 @@ module.exports = function (config) {
|
|
|
98
98
|
}
|
|
99
99
|
await helper.saveScreenshot(fileName, options.fullPageScreenshots);
|
|
100
100
|
|
|
101
|
+
if (!test.artifacts) test.artifacts = {};
|
|
101
102
|
test.artifacts.screenshot = path.join(global.output_dir, fileName);
|
|
102
103
|
if (Container.mocha().options.reporterOptions['mocha-junit-reporter'] && Container.mocha().options.reporterOptions['mocha-junit-reporter'].options.attachments) {
|
|
103
104
|
test.attachments = [path.join(global.output_dir, fileName)];
|
|
@@ -20,6 +20,7 @@ const defaultConfig = {
|
|
|
20
20
|
fullPageScreenshots: false,
|
|
21
21
|
output: global.output_dir,
|
|
22
22
|
screenshotsForAllureReport: false,
|
|
23
|
+
disableScreenshotOnFail: true,
|
|
23
24
|
};
|
|
24
25
|
|
|
25
26
|
const templates = {};
|
|
@@ -56,6 +57,7 @@ const templates = {};
|
|
|
56
57
|
* * `fullPageScreenshots`: should full page screenshots be used. Default: false.
|
|
57
58
|
* * `output`: a directory where reports should be stored. Default: `output`.
|
|
58
59
|
* * `screenshotsForAllureReport`: If Allure plugin is enabled this plugin attaches each saved screenshot to allure report. Default: false.
|
|
60
|
+
* * `disableScreenshotOnFail : Disables the capturing of screeshots after the failed step. Default: true.
|
|
59
61
|
*
|
|
60
62
|
* @param {*} config
|
|
61
63
|
*/
|
|
@@ -80,6 +82,7 @@ module.exports = function (config) {
|
|
|
80
82
|
let error;
|
|
81
83
|
let savedStep = null;
|
|
82
84
|
let currentTest = null;
|
|
85
|
+
let scenarioFailed = false;
|
|
83
86
|
|
|
84
87
|
const recordedTests = {};
|
|
85
88
|
const pad = '0000';
|
|
@@ -135,10 +138,14 @@ module.exports = function (config) {
|
|
|
135
138
|
if (isStepIgnored(step)) return;
|
|
136
139
|
if (savedStep === step) return; // already saved
|
|
137
140
|
// Ignore steps from BeforeSuite function
|
|
141
|
+
if (scenarioFailed && config.disableScreenshotOnFail) return;
|
|
138
142
|
if (step.metaStep && step.metaStep.name === 'BeforeSuite') return;
|
|
139
143
|
|
|
140
144
|
const fileName = `${pad.substring(0, pad.length - stepNum.toString().length) + stepNum.toString()}.png`;
|
|
141
145
|
try {
|
|
146
|
+
if (step.status === 'failed') {
|
|
147
|
+
scenarioFailed = true;
|
|
148
|
+
}
|
|
142
149
|
stepNum++;
|
|
143
150
|
slides[fileName] = step;
|
|
144
151
|
await helper.saveScreenshot(path.relative(reportDir, path.join(dir, fileName)), config.fullPageScreenshots);
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
const event = require('../event');
|
|
2
|
+
|
|
3
|
+
const defaultConfig = {
|
|
4
|
+
timeout: 150,
|
|
5
|
+
force: false,
|
|
6
|
+
noTimeoutSteps: [
|
|
7
|
+
'amOnPage',
|
|
8
|
+
'wait*',
|
|
9
|
+
],
|
|
10
|
+
customTimeoutSteps: [],
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Set timeout for test steps globally.
|
|
15
|
+
*
|
|
16
|
+
* Add this plugin to config file:
|
|
17
|
+
*
|
|
18
|
+
* ```js
|
|
19
|
+
* plugins: {
|
|
20
|
+
* stepTimeout: {
|
|
21
|
+
* enabled: true
|
|
22
|
+
* }
|
|
23
|
+
* }
|
|
24
|
+
* ```
|
|
25
|
+
*
|
|
26
|
+
*
|
|
27
|
+
* Run tests with plugin enabled:
|
|
28
|
+
*
|
|
29
|
+
* ```
|
|
30
|
+
* npx codeceptjs run --plugins stepTimeout
|
|
31
|
+
* ```
|
|
32
|
+
*
|
|
33
|
+
* #### Configuration:
|
|
34
|
+
*
|
|
35
|
+
* * `timeout` - global step timeout, default 150 seconds
|
|
36
|
+
* * `force` - whether to use timeouts set in plugin config to override step timeouts set in code with I.limitTime(x).action(...), default false
|
|
37
|
+
* * `noTimeoutSteps` - an array of steps with no timeout. Default:
|
|
38
|
+
* * `amOnPage`
|
|
39
|
+
* * `wait*`
|
|
40
|
+
*
|
|
41
|
+
* you could set your own noTimeoutSteps which would replace the default one.
|
|
42
|
+
*
|
|
43
|
+
* * `customTimeoutSteps` - an array of step actions with custom timeout. Use it to override or extend noTimeoutSteps.
|
|
44
|
+
* You can use step names or step prefixes ending with `*`. As such, `wait*` will match all steps starting with `wait`.
|
|
45
|
+
*
|
|
46
|
+
* #### Example
|
|
47
|
+
*
|
|
48
|
+
* ```js
|
|
49
|
+
* plugins: {
|
|
50
|
+
* stepTimeout: {
|
|
51
|
+
* enabled: true,
|
|
52
|
+
* force: true,
|
|
53
|
+
* noTimeoutSteps: [
|
|
54
|
+
* 'scroll*', // ignore all scroll steps
|
|
55
|
+
* /Cookie/, // ignore all steps with a Cookie in it (by regexp)
|
|
56
|
+
* ],
|
|
57
|
+
* customTimeoutSteps: [
|
|
58
|
+
* ['myFlakyStep*', 1],
|
|
59
|
+
* ['scrollWhichRequiresTimeout', 5],
|
|
60
|
+
* ]
|
|
61
|
+
* }
|
|
62
|
+
* }
|
|
63
|
+
* ```
|
|
64
|
+
*
|
|
65
|
+
*/
|
|
66
|
+
module.exports = (config) => {
|
|
67
|
+
config = Object.assign(defaultConfig, config);
|
|
68
|
+
// below override rule makes sure customTimeoutSteps go first but then they override noTimeoutSteps in case of exact pattern match
|
|
69
|
+
config.customTimeoutSteps = config.customTimeoutSteps.concat(config.noTimeoutSteps).concat(config.customTimeoutSteps);
|
|
70
|
+
|
|
71
|
+
event.dispatcher.on(event.step.before, (step) => {
|
|
72
|
+
let stepTimeout;
|
|
73
|
+
for (let stepRule of config.customTimeoutSteps) {
|
|
74
|
+
let customTimeout = 0;
|
|
75
|
+
if (Array.isArray(stepRule)) {
|
|
76
|
+
if (stepRule.length > 1) customTimeout = stepRule[1];
|
|
77
|
+
stepRule = stepRule[0];
|
|
78
|
+
}
|
|
79
|
+
if (stepRule instanceof RegExp
|
|
80
|
+
? step.name.match(stepRule)
|
|
81
|
+
: (step.name === stepRule || stepRule.indexOf('*') && step.name.startsWith(stepRule.slice(0, -1)))
|
|
82
|
+
) {
|
|
83
|
+
stepTimeout = customTimeout;
|
|
84
|
+
break;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
stepTimeout = stepTimeout === undefined ? config.timeout : stepTimeout;
|
|
88
|
+
step.totalTimeout = stepTimeout * 1000;
|
|
89
|
+
});
|
|
90
|
+
};
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
const { v4: uuidv4 } = require('uuid');
|
|
2
|
+
const fsPromise = require('fs').promises;
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const event = require('../event');
|
|
5
|
+
|
|
6
|
+
// This will convert a given timestamp in milliseconds to
|
|
7
|
+
// an SRT recognized timestamp, ie HH:mm:ss,SSS
|
|
8
|
+
function formatTimestamp(timestampInMs) {
|
|
9
|
+
const date = new Date(0, 0, 0, 0, 0, 0, timestampInMs);
|
|
10
|
+
const hours = date.getHours();
|
|
11
|
+
const minutes = date.getMinutes();
|
|
12
|
+
const seconds = date.getSeconds();
|
|
13
|
+
const ms = timestampInMs - (hours * 3600000 + minutes * 60000 + seconds * 1000);
|
|
14
|
+
return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')},${ms.toString().padStart(3, '0')}`;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
let steps = {};
|
|
18
|
+
let testStartedAt;
|
|
19
|
+
/**
|
|
20
|
+
* Automatically captures steps as subtitle, and saves it as an artifact when a video is found for a failed test
|
|
21
|
+
*
|
|
22
|
+
* #### Configuration
|
|
23
|
+
* ```js
|
|
24
|
+
* plugins: {
|
|
25
|
+
* subtitles: {
|
|
26
|
+
* enabled: true
|
|
27
|
+
* }
|
|
28
|
+
* }
|
|
29
|
+
* ```
|
|
30
|
+
*/
|
|
31
|
+
module.exports = function () {
|
|
32
|
+
event.dispatcher.on(event.test.before, (_) => {
|
|
33
|
+
testStartedAt = Date.now();
|
|
34
|
+
steps = {};
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
event.dispatcher.on(event.step.started, (step) => {
|
|
38
|
+
const stepStartedAt = Date.now();
|
|
39
|
+
step.id = uuidv4();
|
|
40
|
+
|
|
41
|
+
let title = `${step.actor}.${step.name}(${step.args ? step.args.join(',') : ''})`;
|
|
42
|
+
if (title.length > 100) {
|
|
43
|
+
title = `${title.substring(0, 100)}...`;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
steps[step.id] = {
|
|
47
|
+
start: formatTimestamp(stepStartedAt - testStartedAt),
|
|
48
|
+
startedAt: stepStartedAt,
|
|
49
|
+
title,
|
|
50
|
+
};
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
event.dispatcher.on(event.step.finished, (step) => {
|
|
54
|
+
if (step && step.id && steps[step.id]) {
|
|
55
|
+
steps[step.id].end = formatTimestamp(Date.now() - testStartedAt);
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
event.dispatcher.on(event.test.after, async (test) => {
|
|
60
|
+
if (test && test.artifacts && test.artifacts.video) {
|
|
61
|
+
const stepsSortedByStartTime = Object.values(steps);
|
|
62
|
+
stepsSortedByStartTime.sort((stepA, stepB) => {
|
|
63
|
+
return stepA.startedAt - stepB.startedAt;
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
let subtitle = '';
|
|
67
|
+
|
|
68
|
+
// For an SRT file, every subtitle has to be in the format as mentioned below:
|
|
69
|
+
//
|
|
70
|
+
// 1
|
|
71
|
+
// HH:mm:ss,SSS --> HH:mm:ss,SSS
|
|
72
|
+
// [title]
|
|
73
|
+
stepsSortedByStartTime.forEach((step, index) => {
|
|
74
|
+
if (step.end) {
|
|
75
|
+
subtitle = `${subtitle}${index + 1}
|
|
76
|
+
${step.start} --> ${step.end}
|
|
77
|
+
${step.title}
|
|
78
|
+
|
|
79
|
+
`;
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
const { dir: artifactsDirectory, name: fileName } = path.parse(test.artifacts.video);
|
|
84
|
+
await fsPromise.writeFile(`${artifactsDirectory}/${fileName}.srt`, subtitle);
|
|
85
|
+
test.artifacts.subtitle = `${artifactsDirectory}/${fileName}.srt`;
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
};
|
package/lib/plugin/tryTo.js
CHANGED
|
@@ -89,7 +89,7 @@ function tryTo(callback) {
|
|
|
89
89
|
recorder.session.catch((err) => {
|
|
90
90
|
result = false;
|
|
91
91
|
const msg = err.inspect ? err.inspect() : err.toString();
|
|
92
|
-
debug(`
|
|
92
|
+
debug(`Unsuccessful try > ${msg}`);
|
|
93
93
|
recorder.session.restore('tryTo');
|
|
94
94
|
return result;
|
|
95
95
|
});
|
package/lib/recorder.js
CHANGED
|
@@ -3,6 +3,8 @@ const promiseRetry = require('promise-retry');
|
|
|
3
3
|
|
|
4
4
|
const { log } = require('./output');
|
|
5
5
|
|
|
6
|
+
const MAX_TASKS = 100;
|
|
7
|
+
|
|
6
8
|
let promise;
|
|
7
9
|
let running = false;
|
|
8
10
|
let errFn;
|
|
@@ -116,7 +118,7 @@ module.exports = {
|
|
|
116
118
|
* @inner
|
|
117
119
|
*/
|
|
118
120
|
start(name) {
|
|
119
|
-
|
|
121
|
+
debug(`${currentQueue()}Starting <${name}> session`);
|
|
120
122
|
tasks.push('--->');
|
|
121
123
|
oldPromises.push(promise);
|
|
122
124
|
this.running = true;
|
|
@@ -130,7 +132,7 @@ module.exports = {
|
|
|
130
132
|
*/
|
|
131
133
|
restore(name) {
|
|
132
134
|
tasks.push('<---');
|
|
133
|
-
|
|
135
|
+
debug(`${currentQueue()}Finalize <${name}> session`);
|
|
134
136
|
this.running = false;
|
|
135
137
|
sessionId = null;
|
|
136
138
|
this.catch(errFn);
|
|
@@ -158,10 +160,11 @@ module.exports = {
|
|
|
158
160
|
* undefined: `add(fn)` -> `false` and `add('step',fn)` -> `true`
|
|
159
161
|
* true: it will retries if `retryOpts` set.
|
|
160
162
|
* false: ignore `retryOpts` and won't retry.
|
|
163
|
+
* @param {number} [timeout]
|
|
161
164
|
* @return {Promise<*> | undefined}
|
|
162
165
|
* @inner
|
|
163
166
|
*/
|
|
164
|
-
add(taskName, fn = undefined, force = false, retry = undefined) {
|
|
167
|
+
add(taskName, fn = undefined, force = false, retry = undefined, timeout = undefined) {
|
|
165
168
|
if (typeof taskName === 'function') {
|
|
166
169
|
fn = taskName;
|
|
167
170
|
taskName = fn.toString();
|
|
@@ -172,19 +175,21 @@ module.exports = {
|
|
|
172
175
|
return;
|
|
173
176
|
}
|
|
174
177
|
tasks.push(taskName);
|
|
175
|
-
debug(`${currentQueue()}Queued | ${taskName}`);
|
|
178
|
+
if (process.env.DEBUG) debug(`${currentQueue()}Queued | ${taskName}`);
|
|
176
179
|
|
|
177
180
|
return promise = Promise.resolve(promise).then((res) => {
|
|
178
181
|
const retryOpts = this.retries.slice(-1).pop();
|
|
179
182
|
// no retries or unnamed tasks
|
|
180
183
|
if (!retryOpts || !taskName || !retry) {
|
|
181
|
-
|
|
184
|
+
const [promise, timer] = getTimeoutPromise(timeout, taskName);
|
|
185
|
+
return Promise.race([promise, Promise.resolve(res).then(fn)]).finally(() => clearTimeout(timer));
|
|
182
186
|
}
|
|
183
187
|
|
|
184
188
|
const retryRules = this.retries.slice().reverse();
|
|
185
189
|
return promiseRetry(Object.assign(defaultRetryOptions, retryOpts), (retry, number) => {
|
|
186
190
|
if (number > 1) log(`${currentQueue()}Retrying... Attempt #${number}`);
|
|
187
|
-
|
|
191
|
+
const [promise, timer] = getTimeoutPromise(timeout, taskName);
|
|
192
|
+
return Promise.race([promise, Promise.resolve(res).then(fn)]).finally(() => clearTimeout(timer)).catch((err) => {
|
|
188
193
|
for (const retryObj of retryRules) {
|
|
189
194
|
if (!retryObj.when) return retry(err);
|
|
190
195
|
if (retryObj.when && retryObj.when(err)) return retry(err);
|
|
@@ -296,7 +301,7 @@ module.exports = {
|
|
|
296
301
|
* @inner
|
|
297
302
|
*/
|
|
298
303
|
stop() {
|
|
299
|
-
debug(this.toString());
|
|
304
|
+
if (process.env.DEBUG) debug(this.toString());
|
|
300
305
|
log(`${currentQueue()}Stopping recording promises`);
|
|
301
306
|
running = false;
|
|
302
307
|
},
|
|
@@ -318,7 +323,7 @@ module.exports = {
|
|
|
318
323
|
* @inner
|
|
319
324
|
*/
|
|
320
325
|
scheduled() {
|
|
321
|
-
return tasks.join('\n');
|
|
326
|
+
return tasks.slice(-MAX_TASKS).join('\n');
|
|
322
327
|
},
|
|
323
328
|
|
|
324
329
|
/**
|
|
@@ -341,6 +346,14 @@ module.exports = {
|
|
|
341
346
|
|
|
342
347
|
};
|
|
343
348
|
|
|
349
|
+
function getTimeoutPromise(timeoutMs, taskName) {
|
|
350
|
+
let timer;
|
|
351
|
+
if (timeoutMs) debug(`Timing out in ${timeoutMs}ms`);
|
|
352
|
+
return [new Promise((done, reject) => {
|
|
353
|
+
timer = setTimeout(() => { reject(new Error(`Action ${taskName} was interrupted on step timeout ${timeoutMs}ms`)); }, timeoutMs || 2e9);
|
|
354
|
+
}), timer];
|
|
355
|
+
}
|
|
356
|
+
|
|
344
357
|
function currentQueue() {
|
|
345
358
|
let session = '';
|
|
346
359
|
if (sessionId) session = `<${sessionId}> `;
|