codeceptjs 3.1.3 → 3.2.3
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 +75 -1
- package/README.md +2 -3
- package/bin/codecept.js +1 -0
- package/docs/advanced.md +99 -61
- package/docs/basics.md +27 -2
- package/docs/bdd.md +2 -2
- package/docs/build/Appium.js +62 -0
- package/docs/build/FileSystem.js +12 -1
- package/docs/build/Playwright.js +37 -33
- package/docs/build/Protractor.js +9 -24
- package/docs/build/Puppeteer.js +10 -28
- package/docs/build/REST.js +1 -0
- package/docs/build/WebDriver.js +2 -24
- package/docs/changelog.md +75 -1
- package/docs/configuration.md +8 -8
- package/docs/custom-helpers.md +2 -37
- package/docs/data.md +9 -9
- package/docs/helpers/Appium.md +240 -198
- package/docs/helpers/FileSystem.md +12 -2
- package/docs/helpers/Playwright.md +226 -225
- package/docs/helpers/Puppeteer.md +1 -17
- package/docs/helpers/REST.md +3 -1
- package/docs/helpers/WebDriver.md +1 -17
- package/docs/installation.md +1 -1
- package/docs/mobile-react-native-locators.md +3 -0
- package/docs/mobile.md +11 -11
- package/docs/nightmare.md +3 -3
- package/docs/pageobjects.md +2 -0
- package/docs/playwright.md +4 -4
- package/docs/plugins.md +138 -9
- package/docs/puppeteer.md +5 -5
- package/docs/reports.md +3 -3
- package/docs/testcafe.md +1 -1
- package/docs/translation.md +1 -1
- package/docs/visual.md +2 -2
- package/docs/vue.md +1 -1
- package/docs/webdriver.md +2 -2
- package/lib/actor.js +19 -1
- package/lib/cli.js +25 -20
- package/lib/codecept.js +2 -0
- package/lib/command/info.js +1 -1
- package/lib/command/workers/runTests.js +25 -7
- package/lib/config.js +12 -0
- package/lib/container.js +3 -1
- package/lib/helper/Appium.js +62 -0
- package/lib/helper/FileSystem.js +12 -1
- package/lib/helper/Playwright.js +37 -23
- package/lib/helper/Protractor.js +2 -14
- package/lib/helper/Puppeteer.js +3 -18
- package/lib/helper/REST.js +1 -0
- package/lib/helper/WebDriver.js +2 -14
- package/lib/interfaces/featureConfig.js +3 -0
- package/lib/interfaces/scenarioConfig.js +4 -0
- package/lib/listener/steps.js +21 -3
- package/lib/listener/timeout.js +72 -0
- package/lib/locator.js +3 -0
- package/lib/plugin/allure.js +6 -1
- package/lib/plugin/autoLogin.js +1 -1
- 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 +91 -0
- package/lib/plugin/tryTo.js +6 -0
- package/lib/recorder.js +18 -6
- package/lib/step.js +58 -0
- package/lib/store.js +2 -0
- package/lib/ui.js +2 -2
- package/package.json +4 -6
- package/typings/index.d.ts +8 -1
- package/typings/types.d.ts +149 -164
- 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/helper/Puppeteer.js
CHANGED
|
@@ -264,7 +264,7 @@ class Puppeteer extends Helper {
|
|
|
264
264
|
async _before() {
|
|
265
265
|
this.sessionPages = {};
|
|
266
266
|
recorder.retry({
|
|
267
|
-
retries:
|
|
267
|
+
retries: 3,
|
|
268
268
|
when: err => {
|
|
269
269
|
if (!err || typeof (err.message) !== 'string') {
|
|
270
270
|
return false;
|
|
@@ -554,10 +554,9 @@ class Puppeteer extends Helper {
|
|
|
554
554
|
this.context = null;
|
|
555
555
|
popupStore.clear();
|
|
556
556
|
this.isAuthenticated = false;
|
|
557
|
+
await this.browser.close();
|
|
557
558
|
if (this.isRemoteBrowser) {
|
|
558
559
|
await this.browser.disconnect();
|
|
559
|
-
} else {
|
|
560
|
-
await this.browser.close();
|
|
561
560
|
}
|
|
562
561
|
}
|
|
563
562
|
|
|
@@ -774,11 +773,7 @@ class Puppeteer extends Helper {
|
|
|
774
773
|
}
|
|
775
774
|
|
|
776
775
|
/**
|
|
777
|
-
*
|
|
778
|
-
*
|
|
779
|
-
* ```js
|
|
780
|
-
* I.seeTitleEquals('Test title.');
|
|
781
|
-
* ```
|
|
776
|
+
* {{> seeTitleEquals }}
|
|
782
777
|
*/
|
|
783
778
|
async seeTitleEquals(text) {
|
|
784
779
|
const title = await this.page.title();
|
|
@@ -2220,16 +2215,6 @@ class Puppeteer extends Helper {
|
|
|
2220
2215
|
return this.page.waitForNavigation(opts);
|
|
2221
2216
|
}
|
|
2222
2217
|
|
|
2223
|
-
/**
|
|
2224
|
-
* {{> waitUntil }}
|
|
2225
|
-
*/
|
|
2226
|
-
async waitUntil(fn, sec = null) {
|
|
2227
|
-
console.log('This method will remove in CodeceptJS 1.4; use `waitForFunction` instead!');
|
|
2228
|
-
const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout;
|
|
2229
|
-
const context = await this._getContext();
|
|
2230
|
-
return context.waitForFunction(fn, { timeout: waitTimeout });
|
|
2231
|
-
}
|
|
2232
|
-
|
|
2233
2218
|
async waitUntilExists(locator, sec) {
|
|
2234
2219
|
console.log(`waitUntilExists deprecated:
|
|
2235
2220
|
* use 'waitForElement' to wait for element to be attached
|
package/lib/helper/REST.js
CHANGED
package/lib/helper/WebDriver.js
CHANGED
|
@@ -582,7 +582,7 @@ class WebDriver extends Helper {
|
|
|
582
582
|
this.context = this.root;
|
|
583
583
|
if (this.options.restart && !this.options.manualStart) return this._startBrowser();
|
|
584
584
|
if (!this.isRunning && !this.options.manualStart) return this._startBrowser();
|
|
585
|
-
this.$$ = this.browser.$$.bind(this.browser);
|
|
585
|
+
if (this.browser) this.$$ = this.browser.$$.bind(this.browser);
|
|
586
586
|
return this.browser;
|
|
587
587
|
}
|
|
588
588
|
|
|
@@ -875,7 +875,7 @@ class WebDriver extends Helper {
|
|
|
875
875
|
* I.defineTimeout({ implicit: 10000, pageLoad: 10000, script: 5000 });
|
|
876
876
|
* ```
|
|
877
877
|
*
|
|
878
|
-
* @param {
|
|
878
|
+
* @param {*} timeouts WebDriver timeouts object.
|
|
879
879
|
*/
|
|
880
880
|
defineTimeout(timeouts) {
|
|
881
881
|
return this._defineBrowserTimeout(this.browser, timeouts);
|
|
@@ -2353,18 +2353,6 @@ class WebDriver extends Helper {
|
|
|
2353
2353
|
return this.browser.waitUntil(async () => this.browser.execute(fn, ...args), { timeout: aSec * 1000, timeoutMsg: '' });
|
|
2354
2354
|
}
|
|
2355
2355
|
|
|
2356
|
-
/**
|
|
2357
|
-
* {{> waitUntil }}
|
|
2358
|
-
*/
|
|
2359
|
-
async waitUntil(fn, sec = null, timeoutMsg = null, interval = null) {
|
|
2360
|
-
const aSec = sec || this.options.waitForTimeout;
|
|
2361
|
-
const _interval = typeof interval === 'number' ? interval * 1000 : null;
|
|
2362
|
-
if (isWebDriver5()) {
|
|
2363
|
-
return this.browser.waitUntil(fn, aSec * 1000, timeoutMsg, _interval);
|
|
2364
|
-
}
|
|
2365
|
-
return this.browser.waitUntil(fn, { timeout: aSec * 1000, timeoutMsg, interval: _interval });
|
|
2366
|
-
}
|
|
2367
|
-
|
|
2368
2356
|
/**
|
|
2369
2357
|
* {{> switchTo }}
|
|
2370
2358
|
*/
|
|
@@ -21,6 +21,9 @@ class FeatureConfig {
|
|
|
21
21
|
* @returns {this}
|
|
22
22
|
*/
|
|
23
23
|
timeout(timeout) {
|
|
24
|
+
console.log(`Feature('${this.suite.title}').timeout(${timeout}) is deprecated!`);
|
|
25
|
+
console.log(`Please use Feature('${this.suite.title}', { timeout: ${timeout / 1000} }) instead`);
|
|
26
|
+
console.log('Timeout should be set in seconds');
|
|
24
27
|
this.suite.timeout(timeout);
|
|
25
28
|
return this;
|
|
26
29
|
}
|
|
@@ -45,6 +45,10 @@ class ScenarioConfig {
|
|
|
45
45
|
* @returns {this}
|
|
46
46
|
*/
|
|
47
47
|
timeout(timeout) {
|
|
48
|
+
console.log(`Scenario('${this.test.title}', () => {}).timeout(${timeout}) is deprecated!`);
|
|
49
|
+
console.log(`Please use Scenario('${this.test.title}', { timeout: ${timeout / 1000} }, () => {}) instead`);
|
|
50
|
+
console.log('Timeout should be set in seconds');
|
|
51
|
+
|
|
48
52
|
this.test.timeout(timeout);
|
|
49
53
|
return this;
|
|
50
54
|
}
|
package/lib/listener/steps.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
const debug = require('debug')('codeceptjs:steps');
|
|
1
2
|
const event = require('../event');
|
|
2
3
|
const store = require('../store');
|
|
4
|
+
const output = require('../output');
|
|
3
5
|
|
|
4
6
|
let currentTest;
|
|
5
7
|
let currentHook;
|
|
@@ -9,6 +11,7 @@ let currentHook;
|
|
|
9
11
|
*/
|
|
10
12
|
module.exports = function () {
|
|
11
13
|
event.dispatcher.on(event.test.before, (test) => {
|
|
14
|
+
test.startedAt = +new Date();
|
|
12
15
|
test.artifacts = {};
|
|
13
16
|
});
|
|
14
17
|
|
|
@@ -19,17 +22,23 @@ module.exports = function () {
|
|
|
19
22
|
else currentTest.retryNum += 1;
|
|
20
23
|
});
|
|
21
24
|
|
|
22
|
-
event.dispatcher.on(event.test.after, () => {
|
|
25
|
+
event.dispatcher.on(event.test.after, (test) => {
|
|
23
26
|
currentTest = null;
|
|
24
27
|
});
|
|
25
28
|
|
|
26
|
-
event.dispatcher.on(event.
|
|
27
|
-
currentHook = null;
|
|
29
|
+
event.dispatcher.on(event.test.finished, (test) => {
|
|
28
30
|
});
|
|
29
31
|
|
|
30
32
|
event.dispatcher.on(event.hook.started, (suite) => {
|
|
31
33
|
currentHook = suite.ctx.test;
|
|
32
34
|
currentHook.steps = [];
|
|
35
|
+
|
|
36
|
+
if (suite.ctx && suite.ctx.test) output.log(`--- STARTED ${suite.ctx.test.title} ---`);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
event.dispatcher.on(event.hook.passed, (suite) => {
|
|
40
|
+
currentHook = null;
|
|
41
|
+
if (suite.ctx && suite.ctx.test) output.log(`--- ENDED ${suite.ctx.test.title} ---`);
|
|
33
42
|
});
|
|
34
43
|
|
|
35
44
|
event.dispatcher.on(event.test.failed, () => {
|
|
@@ -51,6 +60,7 @@ module.exports = function () {
|
|
|
51
60
|
});
|
|
52
61
|
|
|
53
62
|
event.dispatcher.on(event.test.passed, () => {
|
|
63
|
+
if (!currentTest) return;
|
|
54
64
|
// To be sure that passed test will be passed in report
|
|
55
65
|
delete currentTest.err;
|
|
56
66
|
currentTest.state = 'passed';
|
|
@@ -58,10 +68,18 @@ module.exports = function () {
|
|
|
58
68
|
|
|
59
69
|
event.dispatcher.on(event.step.started, (step) => {
|
|
60
70
|
if (store.debugMode) return;
|
|
71
|
+
step.startedAt = +new Date();
|
|
61
72
|
if (currentHook && Array.isArray(currentHook.steps)) {
|
|
62
73
|
return currentHook.steps.push(step);
|
|
63
74
|
}
|
|
64
75
|
if (!currentTest || !currentTest.steps) return;
|
|
65
76
|
currentTest.steps.push(step);
|
|
66
77
|
});
|
|
78
|
+
|
|
79
|
+
event.dispatcher.on(event.step.finished, (step) => {
|
|
80
|
+
if (store.debugMode) return;
|
|
81
|
+
step.finishedAt = +new Date();
|
|
82
|
+
if (step.startedAt) step.duration = step.finishedAt - step.startedAt;
|
|
83
|
+
debug(`Step '${step}' finished; Duration: ${step.duration}ms`);
|
|
84
|
+
});
|
|
67
85
|
};
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
const event = require('../event');
|
|
2
|
+
const output = require('../output');
|
|
3
|
+
const recorder = require('../recorder');
|
|
4
|
+
const Config = require('../config');
|
|
5
|
+
const { timeouts } = require('../store');
|
|
6
|
+
const TIMEOUT_ORDER = require('../step').TIMEOUT_ORDER;
|
|
7
|
+
|
|
8
|
+
module.exports = function () {
|
|
9
|
+
let timeout;
|
|
10
|
+
let timeoutStack = [];
|
|
11
|
+
let currentTest;
|
|
12
|
+
let currentTimeout;
|
|
13
|
+
|
|
14
|
+
if (!timeouts) {
|
|
15
|
+
console.log('Timeouts were disabled');
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
event.dispatcher.on(event.suite.before, (suite) => {
|
|
20
|
+
timeoutStack = [];
|
|
21
|
+
const globalTimeout = Config.get('timeout');
|
|
22
|
+
if (globalTimeout) {
|
|
23
|
+
if (globalTimeout >= 1000) {
|
|
24
|
+
console.log(`Warning: Timeout was set to ${globalTimeout}secs.\nGlobal timeout should be specified in seconds.`);
|
|
25
|
+
}
|
|
26
|
+
timeoutStack.push(globalTimeout);
|
|
27
|
+
}
|
|
28
|
+
if (suite.totalTimeout) timeoutStack.push(suite.totalTimeout);
|
|
29
|
+
output.log(`Timeouts: ${timeoutStack}`);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
event.dispatcher.on(event.test.before, (test) => {
|
|
33
|
+
currentTest = test;
|
|
34
|
+
timeout = test.totalTimeout || timeoutStack[timeoutStack.length - 1];
|
|
35
|
+
if (!timeout) return;
|
|
36
|
+
currentTimeout = timeout;
|
|
37
|
+
output.debug(`Test Timeout: ${timeout}s`);
|
|
38
|
+
timeout *= 1000;
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
event.dispatcher.on(event.test.passed, (test) => {
|
|
42
|
+
currentTest = null;
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
event.dispatcher.on(event.test.failed, (test) => {
|
|
46
|
+
currentTest = null;
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
event.dispatcher.on(event.step.before, (step) => {
|
|
50
|
+
if (typeof timeout !== 'number') return;
|
|
51
|
+
|
|
52
|
+
if (timeout < 0) {
|
|
53
|
+
step.setTimeout(0.01, TIMEOUT_ORDER.testOrSuite);
|
|
54
|
+
} else {
|
|
55
|
+
step.setTimeout(timeout, TIMEOUT_ORDER.testOrSuite);
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
event.dispatcher.on(event.step.finished, (step) => {
|
|
60
|
+
if (typeof timeout === 'number' && !Number.isNaN(timeout)) timeout -= step.duration;
|
|
61
|
+
|
|
62
|
+
if (typeof timeout === 'number' && timeout <= 0 && recorder.isRunning()) {
|
|
63
|
+
if (currentTest && currentTest.callback) {
|
|
64
|
+
recorder.reset();
|
|
65
|
+
// replace mocha timeout with custom timeout
|
|
66
|
+
currentTest.timeout(0);
|
|
67
|
+
currentTest.callback(new Error(`Timeout ${currentTimeout}s exceeded (with Before hook)`));
|
|
68
|
+
currentTest.timedOut = true;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
};
|
package/lib/locator.js
CHANGED
|
@@ -313,11 +313,14 @@ Locator.build = (locator) => {
|
|
|
313
313
|
|
|
314
314
|
/**
|
|
315
315
|
* Filters to modify locators
|
|
316
|
+
* @type {Array<function(CodeceptJS.LocatorOrString, Locator): void>}
|
|
316
317
|
*/
|
|
317
318
|
Locator.filters = [];
|
|
318
319
|
|
|
319
320
|
/**
|
|
320
321
|
* Appends new `Locator` filter to an `Locator.filters` array, and returns the new length of the array.
|
|
322
|
+
* @param {function(CodeceptJS.LocatorOrString, Locator): void} fn
|
|
323
|
+
* @returns {number}
|
|
321
324
|
*/
|
|
322
325
|
Locator.addFilter = fn => Locator.filters.push(fn);
|
|
323
326
|
|
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) {
|
package/lib/plugin/autoLogin.js
CHANGED
|
@@ -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,91 @@
|
|
|
1
|
+
const event = require('../event');
|
|
2
|
+
const TIMEOUT_ORDER = require('../step').TIMEOUT_ORDER;
|
|
3
|
+
|
|
4
|
+
const defaultConfig = {
|
|
5
|
+
timeout: 150,
|
|
6
|
+
overrideStepLimits: false,
|
|
7
|
+
noTimeoutSteps: [
|
|
8
|
+
'amOnPage',
|
|
9
|
+
'wait*',
|
|
10
|
+
],
|
|
11
|
+
customTimeoutSteps: [],
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Set timeout for test steps globally.
|
|
16
|
+
*
|
|
17
|
+
* Add this plugin to config file:
|
|
18
|
+
*
|
|
19
|
+
* ```js
|
|
20
|
+
* plugins: {
|
|
21
|
+
* stepTimeout: {
|
|
22
|
+
* enabled: true
|
|
23
|
+
* }
|
|
24
|
+
* }
|
|
25
|
+
* ```
|
|
26
|
+
*
|
|
27
|
+
*
|
|
28
|
+
* Run tests with plugin enabled:
|
|
29
|
+
*
|
|
30
|
+
* ```
|
|
31
|
+
* npx codeceptjs run --plugins stepTimeout
|
|
32
|
+
* ```
|
|
33
|
+
*
|
|
34
|
+
* #### Configuration:
|
|
35
|
+
*
|
|
36
|
+
* * `timeout` - global step timeout, default 150 seconds
|
|
37
|
+
* * `overrideStepLimits` - whether to use timeouts set in plugin config to override step timeouts set in code with I.limitTime(x).action(...), default false
|
|
38
|
+
* * `noTimeoutSteps` - an array of steps with no timeout. Default:
|
|
39
|
+
* * `amOnPage`
|
|
40
|
+
* * `wait*`
|
|
41
|
+
*
|
|
42
|
+
* you could set your own noTimeoutSteps which would replace the default one.
|
|
43
|
+
*
|
|
44
|
+
* * `customTimeoutSteps` - an array of step actions with custom timeout. Use it to override or extend noTimeoutSteps.
|
|
45
|
+
* You can use step names or step prefixes ending with `*`. As such, `wait*` will match all steps starting with `wait`.
|
|
46
|
+
*
|
|
47
|
+
* #### Example
|
|
48
|
+
*
|
|
49
|
+
* ```js
|
|
50
|
+
* plugins: {
|
|
51
|
+
* stepTimeout: {
|
|
52
|
+
* enabled: true,
|
|
53
|
+
* overrideStepLimits: true,
|
|
54
|
+
* noTimeoutSteps: [
|
|
55
|
+
* 'scroll*', // ignore all scroll steps
|
|
56
|
+
* /Cookie/, // ignore all steps with a Cookie in it (by regexp)
|
|
57
|
+
* ],
|
|
58
|
+
* customTimeoutSteps: [
|
|
59
|
+
* ['myFlakyStep*', 1],
|
|
60
|
+
* ['scrollWhichRequiresTimeout', 5],
|
|
61
|
+
* ]
|
|
62
|
+
* }
|
|
63
|
+
* }
|
|
64
|
+
* ```
|
|
65
|
+
*
|
|
66
|
+
*/
|
|
67
|
+
module.exports = (config) => {
|
|
68
|
+
config = Object.assign(defaultConfig, config);
|
|
69
|
+
// below override rule makes sure customTimeoutSteps go first but then they override noTimeoutSteps in case of exact pattern match
|
|
70
|
+
config.customTimeoutSteps = config.customTimeoutSteps.concat(config.noTimeoutSteps).concat(config.customTimeoutSteps);
|
|
71
|
+
|
|
72
|
+
event.dispatcher.on(event.step.before, (step) => {
|
|
73
|
+
let stepTimeout;
|
|
74
|
+
for (let stepRule of config.customTimeoutSteps) {
|
|
75
|
+
let customTimeout = 0;
|
|
76
|
+
if (Array.isArray(stepRule)) {
|
|
77
|
+
if (stepRule.length > 1) customTimeout = stepRule[1];
|
|
78
|
+
stepRule = stepRule[0];
|
|
79
|
+
}
|
|
80
|
+
if (stepRule instanceof RegExp
|
|
81
|
+
? step.name.match(stepRule)
|
|
82
|
+
: (step.name === stepRule || stepRule.indexOf('*') && step.name.startsWith(stepRule.slice(0, -1)))
|
|
83
|
+
) {
|
|
84
|
+
stepTimeout = customTimeout;
|
|
85
|
+
break;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
stepTimeout = stepTimeout === undefined ? config.timeout : stepTimeout;
|
|
89
|
+
step.setTimeout(stepTimeout * 1000, config.overrideStepLimits ? TIMEOUT_ORDER.stepTimeoutHard : TIMEOUT_ORDER.stepTimeoutSoft);
|
|
90
|
+
});
|
|
91
|
+
};
|
package/lib/plugin/tryTo.js
CHANGED
|
@@ -42,6 +42,12 @@ const defaultConfig = {
|
|
|
42
42
|
* #### Multiple Conditional Assertions
|
|
43
43
|
*
|
|
44
44
|
* ```js
|
|
45
|
+
*
|
|
46
|
+
* Add assert requires first:
|
|
47
|
+
* ```js
|
|
48
|
+
* const assert = require('assert');
|
|
49
|
+
* ```
|
|
50
|
+
* Then use the assert:
|
|
45
51
|
* const result1 = await tryTo(() => I.see('Hello, user'));
|
|
46
52
|
* const result2 = await tryTo(() => I.seeElement('.welcome'));
|
|
47
53
|
* assert.ok(result1 && result2, 'Assertions were not succesful');
|
package/lib/recorder.js
CHANGED
|
@@ -118,7 +118,7 @@ module.exports = {
|
|
|
118
118
|
* @inner
|
|
119
119
|
*/
|
|
120
120
|
start(name) {
|
|
121
|
-
|
|
121
|
+
debug(`${currentQueue()}Starting <${name}> session`);
|
|
122
122
|
tasks.push('--->');
|
|
123
123
|
oldPromises.push(promise);
|
|
124
124
|
this.running = true;
|
|
@@ -132,7 +132,7 @@ module.exports = {
|
|
|
132
132
|
*/
|
|
133
133
|
restore(name) {
|
|
134
134
|
tasks.push('<---');
|
|
135
|
-
|
|
135
|
+
debug(`${currentQueue()}Finalize <${name}> session`);
|
|
136
136
|
this.running = false;
|
|
137
137
|
sessionId = null;
|
|
138
138
|
this.catch(errFn);
|
|
@@ -160,10 +160,11 @@ module.exports = {
|
|
|
160
160
|
* undefined: `add(fn)` -> `false` and `add('step',fn)` -> `true`
|
|
161
161
|
* true: it will retries if `retryOpts` set.
|
|
162
162
|
* false: ignore `retryOpts` and won't retry.
|
|
163
|
+
* @param {number} [timeout]
|
|
163
164
|
* @return {Promise<*> | undefined}
|
|
164
165
|
* @inner
|
|
165
166
|
*/
|
|
166
|
-
add(taskName, fn = undefined, force = false, retry = undefined) {
|
|
167
|
+
add(taskName, fn = undefined, force = false, retry = undefined, timeout = undefined) {
|
|
167
168
|
if (typeof taskName === 'function') {
|
|
168
169
|
fn = taskName;
|
|
169
170
|
taskName = fn.toString();
|
|
@@ -177,16 +178,19 @@ module.exports = {
|
|
|
177
178
|
if (process.env.DEBUG) debug(`${currentQueue()}Queued | ${taskName}`);
|
|
178
179
|
|
|
179
180
|
return promise = Promise.resolve(promise).then((res) => {
|
|
180
|
-
|
|
181
|
+
// prefer options for non-conditional retries
|
|
182
|
+
const retryOpts = this.retries.sort((r1, r2) => r1.when && !r2.when).slice(-1).pop();
|
|
181
183
|
// no retries or unnamed tasks
|
|
182
184
|
if (!retryOpts || !taskName || !retry) {
|
|
183
|
-
|
|
185
|
+
const [promise, timer] = getTimeoutPromise(timeout, taskName);
|
|
186
|
+
return Promise.race([promise, Promise.resolve(res).then(fn)]).finally(() => clearTimeout(timer));
|
|
184
187
|
}
|
|
185
188
|
|
|
186
189
|
const retryRules = this.retries.slice().reverse();
|
|
187
190
|
return promiseRetry(Object.assign(defaultRetryOptions, retryOpts), (retry, number) => {
|
|
188
191
|
if (number > 1) log(`${currentQueue()}Retrying... Attempt #${number}`);
|
|
189
|
-
|
|
192
|
+
const [promise, timer] = getTimeoutPromise(timeout, taskName);
|
|
193
|
+
return Promise.race([promise, Promise.resolve(res).then(fn)]).finally(() => clearTimeout(timer)).catch((err) => {
|
|
190
194
|
for (const retryObj of retryRules) {
|
|
191
195
|
if (!retryObj.when) return retry(err);
|
|
192
196
|
if (retryObj.when && retryObj.when(err)) return retry(err);
|
|
@@ -343,6 +347,14 @@ module.exports = {
|
|
|
343
347
|
|
|
344
348
|
};
|
|
345
349
|
|
|
350
|
+
function getTimeoutPromise(timeoutMs, taskName) {
|
|
351
|
+
let timer;
|
|
352
|
+
if (timeoutMs) debug(`Timing out in ${timeoutMs}ms`);
|
|
353
|
+
return [new Promise((done, reject) => {
|
|
354
|
+
timer = setTimeout(() => { reject(new Error(`Action ${taskName} was interrupted on step timeout ${timeoutMs}ms`)); }, timeoutMs || 2e9);
|
|
355
|
+
}), timer];
|
|
356
|
+
}
|
|
357
|
+
|
|
346
358
|
function currentQueue() {
|
|
347
359
|
let session = '';
|
|
348
360
|
if (sessionId) session = `<${sessionId}> `;
|