Skip to content

Commit 3923aad

Browse files
authored
Retries API and fix for Puppeteer (#936)
* retries API and fix for Puppeteer * fixed linting * added documentation on retries * fixed retries issues, added tests, fixed within async/await * fixed linting * fixed puppeteer test
1 parent ab24a51 commit 3923aad

17 files changed

+398
-48
lines changed

CHANGELOG.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,24 @@
1+
## 1.1.5
2+
3+
* [Puppeteer] Rerun steps failed due to "Cannot find context with specified id" Error.
4+
* Added syntax to retry a single step:
5+
6+
```js
7+
// retry action once on failure
8+
I.retry().see('Hello');
9+
// retry action 3 times on failure
10+
I.retry(3).see('Hello');
11+
// retry action 3 times waiting for 0.1 second before next try
12+
I.retry({ retries: 3, minTimeout: 100 }).see('Hello');
13+
// retry action 3 times waiting no more than 3 seconds for last retry
14+
I.retry({ retries: 3, maxTimeout: 3000 }).see('Hello');
15+
//
16+
I.retry({
17+
retries: 2,
18+
when: err => err.message === 'Node not visible'
19+
}).seeElement('#user');
20+
```
21+
122
## 1.1.4
223

324
* Removed `yarn` call in package.json

docs/basics.md

Lines changed: 46 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,8 @@ Scenario("Don't stop me", {timeout: 0}, (I) => {});
204204

205205
### Retries
206206

207+
#### Retry Feature
208+
207209
Browser tests can be very fragile and some time you need to re run the few times just to make them pass.
208210
This can be done with `retries` option added to `Feature` declaration.
209211

@@ -212,25 +214,60 @@ You can set number of a retries for a feature:
212214

213215
```js
214216
Feature('Complex JS Stuff', {retries: 3})
217+
218+
219+
Scenario('Really complex', (I) => {
220+
// test goes here
221+
});
215222
```
216223

217224
Every Scenario inside this feature will be rerun 3 times.
218-
You can make an exception for a specific scenario by passing `retries` option to it:
225+
You can make an exception for a specific scenario by passing `retries` option to a Scenario.
219226

220-
```js
221-
Feature('Complex JS Stuff', {retries: 3})
227+
#### Retry Scenario
222228

223-
Scenario('Not that complex', {retries: 1}, (I) => {
229+
```js
230+
Scenario('Really complex', {retries: 2}, (I) => {
224231
// test goes here
225232
});
226233

227-
Scenario('Really complex', (I) => {
228-
// test goes here
229-
});
230234
```
231235

232-
"Really complex" test will be restarted 3 times,
233-
while "Not that complex" test will be rerun only once.
236+
This scenario will be restarted two times on a failure
237+
238+
#### Retry Step
239+
240+
If you have a step which often fails you can retry execution for this single step.
241+
Use `retry()` function before an action to ask CodeceptJS to retry this step on failure:
242+
243+
```js
244+
I.retry().see('Welcome');
245+
```
246+
247+
If you'd like to retry step more than once pass the amount as parameter:
248+
249+
```js
250+
I.retry(3).see('Welcome');
251+
```
252+
253+
Additional options can be provided to retry so you can set the additional options (defined in [promise-retry](https://www.npmjs.com/package/promise-retry) library).
254+
255+
256+
```js
257+
// retry action 3 times waiting for 0.1 second before next try
258+
I.retry({ retries: 3, minTimeout: 100 }).see('Hello');
259+
260+
// retry action 3 times waiting no more than 3 seconds for last retry
261+
I.retry({ retries: 3, maxTimeout: 3000 }).see('Hello');
262+
263+
// retry 2 times if error with message 'Node not visible' happens
264+
I.retry({
265+
retries: 2,
266+
when: err => err.message === 'Node not visible'
267+
}).seeElement('#user');
268+
```
269+
270+
Pass a function to `when` option to retry only when error matches the expected one.
234271

235272
---
236273

docs/helpers.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,4 +228,27 @@ class JSWait extends codecept_helper {
228228
module.exports = JSWait;
229229
```
230230

231+
## Conditional Retries
232+
233+
It is possible to execute global conditional retries to handle unforseen errors.
234+
Lost connections and network issues are good candidates to be retried whenever they appear.
235+
236+
This can be done inside a helper using the global [promise recorder](https://codecept.io/hooks/#api):
237+
238+
Example: Retrying rendering errors in Puppeteer.
239+
240+
```js
241+
_before() {
242+
const recorder = require('codeceptjs').recorder;
243+
recorder.retry({
244+
retries: 2,
245+
when: err => err.message.indexOf('Cannot find context with specified id') > -1,
246+
});
247+
}
248+
```
249+
250+
`recorder.retry` acts similarly to `I.retry()` and accepts the same parameters. It expects the `when` parameter to be set so it would handle only specific errors and not to retry for every failed step.
251+
252+
Retry rules are available in array `recorder.retries`. The last retry rule can be disabled by running `recorder.retries.pop()`;
253+
231254
### done()

docs/hooks.md

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,38 @@ module.exports = function() {
251251
252252
Whenever you execute tests with `--verbose` option you will see registered events and promises executed by a recorder.
253253
254+
255+
### Conditional Retries with Recorder
256+
257+
It is possible to execute global conditional retries to handle unforseen errors.
258+
Lost connections and network issues are good candidates to be retried whenever they appear.
259+
260+
This can be done inside a [helper](https://codecept.io/helpers/) using `recorder`:
261+
262+
Example: Retrying rendering errors in Puppeteer.
263+
264+
```js
265+
_before() {
266+
const recorder = require('codeceptjs').recorder;
267+
recorder.retry({
268+
retries: 2,
269+
when: err => err.message.indexOf('Cannot find context with specified id') > -1,
270+
});
271+
}
272+
```
273+
274+
`recorder.retry` acts similarly to `I.retry()` and accepts the same parameters. It expects the `when` parameter to be set so it would handle only specific errors and not to retry for every failed step.
275+
276+
Retry rules are available in array `recorder.retries`. The last retry rule can be disabled by running `recorder.retries.pop()`;
277+
278+
```js
279+
// inside a helper
280+
disableLastRetryRule() {
281+
const recorder = require('codeceptjs').recorder;
282+
recorder.retries.pop();
283+
}
284+
```
285+
254286
### Output
255287
256288
Output module provides 4 verbosity levels. Depending on the mode you can have different information printed using corresponding functions.

lib/actor.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,10 +38,19 @@ module.exports = function (obj) {
3838

3939
// add print comment method`
4040
obj.say = msg => recorder.add(`say ${msg}`, () => output.say(msg));
41+
obj.retry = retryStep;
4142

4243
return obj;
4344
};
4445

46+
function retryStep(opts) {
47+
if (opts === undefined) opts = 1;
48+
recorder.retry(opts);
49+
// remove retry once the step passed
50+
recorder.add(_ => event.dispatcher.once(event.step.passed, _ => recorder.retries.pop()));
51+
return this;
52+
}
53+
4554
function recordStep(step, args) {
4655
step.status = 'queued';
4756
step.setArguments(args);

lib/helper/Puppeteer.js

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
const requireg = require('requireg');
22
const Helper = require('../helper');
33
const Locator = require('../locator');
4+
const recorder = require('../recorder');
45
const stringIncludes = require('../assert/include').includes;
56
const { urlEquals } = require('../assert/equal');
67
const { equals } = require('../assert/equal');
@@ -19,8 +20,8 @@ const ElementNotFound = require('./errors/ElementNotFound');
1920
const Popup = require('./extras/Popup');
2021
const Console = require('./extras/Console');
2122

22-
const puppeteer = requireg('puppeteer');
2323

24+
let puppeteer;
2425
const popupStore = new Popup();
2526
const consoleLogStore = new Console();
2627

@@ -58,6 +59,7 @@ const consoleLogStore = new Console();
5859
class Puppeteer extends Helper {
5960
constructor(config) {
6061
super(config);
62+
puppeteer = requireg('puppeteer');
6163

6264
// set defaults
6365
this.options = {
@@ -92,7 +94,7 @@ class Puppeteer extends Helper {
9294
try {
9395
requireg('puppeteer');
9496
} catch (e) {
95-
return ['puppeteer'];
97+
return ['puppeteer@^1.0.0'];
9698
}
9799
}
98100

@@ -108,6 +110,10 @@ class Puppeteer extends Helper {
108110

109111

110112
async _before() {
113+
recorder.retry({
114+
retries: 2,
115+
when: err => err.message.indexOf('Cannot find context with specified id') > -1,
116+
});
111117
if (this.options.restart && !this.options.manualStart) return this._startBrowser();
112118
if (!this.isRunning && !this.options.manualStart) return this._startBrowser();
113119
return this.browser;
@@ -922,7 +928,7 @@ class Puppeteer extends Helper {
922928
* Get JS log from browser.
923929
*
924930
* ```js
925-
* let logs = yield I.grabBrowserLogs();
931+
* let logs = await I.grabBrowserLogs();
926932
* console.log(JSON.stringify(logs))
927933
* ```
928934
*/

lib/recorder.js

Lines changed: 45 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,30 @@
1+
const promiseRetry = require('promise-retry');
2+
const log = require('./output').log;
3+
14
let promise;
25
let running = false;
36
let errFn;
47
let queueId = 0;
58
let sessionId = null;
69
let asyncErr = null;
7-
const log = require('./output').log;
810

911
let tasks = [];
10-
1112
let oldPromises = [];
1213

14+
const defaultRetryOptions = {
15+
retries: 0,
16+
minTimeout: 150,
17+
maxTimeout: 10000,
18+
};
19+
20+
1321
/**
1422
* Singleton object to record all test steps as promises and run them in chain.
1523
*/
1624
module.exports = {
1725

26+
retries: [],
27+
1828
/**
1929
* Start recording promises
2030
*
@@ -63,6 +73,7 @@ module.exports = {
6373
oldPromises = [];
6474
tasks = [];
6575
this.session.running = false;
76+
this.retries = [];
6677
},
6778

6879
session: {
@@ -109,8 +120,38 @@ module.exports = {
109120
}
110121
tasks.push(taskName);
111122
log(`${currentQueue()}Queued | ${taskName}`);
112-
// ensure a valid promise is always in chain
113-
return promise = Promise.resolve(promise).then(fn);
123+
124+
return promise = Promise.resolve(promise).then((res) => {
125+
const retryOpts = this.retries.slice(-1).pop();
126+
// no retries or unnamed tasks
127+
if (!retryOpts || !taskName) {
128+
return Promise.resolve(res).then(fn);
129+
}
130+
131+
return promiseRetry(Object.assign(defaultRetryOptions, retryOpts), (retry, number) => {
132+
if (number > 1) log(`${currentQueue()}Retrying... Attempt #${number}`);
133+
return Promise.resolve(res).then(fn).catch((err) => {
134+
// does retry handled or should be thrown
135+
for (const retryObj in this.retries.reverse()) {
136+
if (!retryObj.when) return retry(err);
137+
if (retryObj.when && retryObj.when(err)) return retry(err);
138+
}
139+
throw err;
140+
});
141+
});
142+
});
143+
},
144+
145+
retry(opts) {
146+
if (!promise) return;
147+
148+
if (opts === null) {
149+
opts = {};
150+
}
151+
if (Number.isInteger(opts)) {
152+
opts = { retries: opts };
153+
}
154+
return promise.then(() => this.retries.push(opts));
114155
},
115156

116157
catch(customErrFn) {

0 commit comments

Comments
 (0)