Skip to content

Commit 8cd9463

Browse files
committedSep 13, 2023
implement repeats
Refs: Re-run a test multiple times mochajs#2332
1 parent 37deed2 commit 8cd9463

34 files changed

+594
-2
lines changed
 

‎docs/index.md

+27-1
Original file line numberDiff line numberDiff line change
@@ -687,6 +687,31 @@ describe('retries', function () {
687687
});
688688
```
689689

690+
## Repeat Tests
691+
692+
Tests can also be repeated when they pass. This feature can be used to test for leaks and proper tear-down procedures. In this case a test is considered to be successful only if all the runs are successful.
693+
694+
This feature does re-run a passed test and its corresponding `beforeEach/afterEach` hooks, but not `before/after` hooks.
695+
696+
If using both `repeat` and `retries`, the test will be run `repeat` times tolerating up to `retries` failures in total.
697+
698+
```js
699+
describe('repeat', function () {
700+
// Repeat all tests in this suite 4 times
701+
this.repeats(4);
702+
703+
beforeEach(function () {
704+
browser.get('http://www.yahoo.com');
705+
});
706+
707+
it('should use proper tear-down', function () {
708+
// Specify this test to only retry up to 2 times
709+
this.repeats(2);
710+
expect($('.foo').isDisplayed()).to.eventually.be.true;
711+
});
712+
});
713+
```
714+
690715
## Dynamically Generating Tests
691716

692717
Given Mocha's use of function expressions to define suites and test cases, it's straightforward to generate your tests dynamically. No special syntax is required — plain ol' JavaScript can be used to achieve functionality similar to "parameterized" tests, which you may have seen in other frameworks.
@@ -1777,7 +1802,7 @@ describe('Array', function () {
17771802
it('should not throw an error', function () {
17781803
(function () {
17791804
[1, 2, 3].indexOf(4);
1780-
}.should.not.throw());
1805+
}).should.not.throw();
17811806
});
17821807
it('should return -1', function () {
17831808
[1, 2, 3].indexOf(4).should.equal(-1);
@@ -2152,6 +2177,7 @@ mocha.setup({
21522177
forbidPending: true,
21532178
global: ['MyLib'],
21542179
retries: 3,
2180+
repeats: 1,
21552181
rootHooks: { beforeEach(done) { ... done();} },
21562182
slow: '100',
21572183
timeout: '2000',

‎example/config/.mocharc.js

+1
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ module.exports = {
3636
'reporter-option': ['foo=bar', 'baz=quux'], // array, not object
3737
require: '@babel/register',
3838
retries: 1,
39+
repeats: 1,
3940
slow: '75',
4041
sort: false,
4142
spec: ['test/**/*.spec.js'], // the positional arguments!

‎example/config/.mocharc.yml

+1
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ reporter-option: # array, not object
4040
- 'baz=quux'
4141
require: '@babel/register'
4242
retries: 1
43+
repeats: 1
4344
slow: '75'
4445
sort: false
4546
spec:

‎lib/cli/run-option-metadata.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ const TYPES = (exports.types = {
4949
'sort',
5050
'watch'
5151
],
52-
number: ['retries', 'jobs'],
52+
number: ['retries', 'repeats', 'jobs'],
5353
string: [
5454
'config',
5555
'fgrep',

‎lib/cli/run.js

+4
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,10 @@ exports.builder = yargs =>
231231
description: 'Retry failed tests this many times',
232232
group: GROUPS.RULES
233233
},
234+
repeats: {
235+
description: 'Repeat passed tests this many times',
236+
group: GROUPS.RULES
237+
},
234238
slow: {
235239
default: defaults.slow,
236240
description: 'Specify "slow" test threshold (in milliseconds)',

‎lib/context.js

+15
Original file line numberDiff line numberDiff line change
@@ -84,3 +84,18 @@ Context.prototype.retries = function (n) {
8484
this.runnable().retries(n);
8585
return this;
8686
};
87+
88+
/**
89+
* Set or get a number of repeats on passed tests
90+
*
91+
* @private
92+
* @param {number} n
93+
* @return {Context} self
94+
*/
95+
Context.prototype.repeats = function (n) {
96+
if (!arguments.length) {
97+
return this.runnable().repeats();
98+
}
99+
this.runnable().repeats(n);
100+
return this;
101+
};

‎lib/hook.js

+1
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ Hook.prototype.error = function (err) {
6363
Hook.prototype.serialize = function serialize() {
6464
return {
6565
$$currentRetry: this.currentRetry(),
66+
$$currentRepeat: this.currentRepeat(),
6667
$$fullTitle: this.fullTitle(),
6768
$$isPending: Boolean(this.isPending()),
6869
$$titlePath: this.titlePath(),

‎lib/mocha.js

+24
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,7 @@ exports.run = function (...args) {
170170
* @param {string|constructor} [options.reporter] - Reporter name or constructor.
171171
* @param {Object} [options.reporterOption] - Reporter settings object.
172172
* @param {number} [options.retries] - Number of times to retry failed tests.
173+
* @param {number} [options.repeat] - Number of times to repeat passed tests.
173174
* @param {number} [options.slow] - Slow threshold value.
174175
* @param {number|string} [options.timeout] - Timeout threshold value.
175176
* @param {string} [options.ui] - Interface name.
@@ -207,6 +208,10 @@ function Mocha(options = {}) {
207208
this.retries(options.retries);
208209
}
209210

211+
if ('repeats' in options) {
212+
this.repeats(options.repeats);
213+
}
214+
210215
[
211216
'allowUncaught',
212217
'asyncOnly',
@@ -763,6 +768,25 @@ Mocha.prototype.retries = function (retry) {
763768
return this;
764769
};
765770

771+
/**
772+
* Sets the number of times to repeat passed tests.
773+
*
774+
* @public
775+
* @see [CLI option](../#-repeats-n)
776+
* @see [Repeat Tests](../#repeat-tests)
777+
* @param {number} repeats - Number of times to repeat passed tests.
778+
* @return {Mocha} this
779+
* @chainable
780+
* @example
781+
*
782+
* // Allow any passed test to be repeated multiple times
783+
* mocha.repeats(1);
784+
*/
785+
Mocha.prototype.repeats = function (repeats) {
786+
this.suite.repeats(repeats);
787+
return this;
788+
};
789+
766790
/**
767791
* Sets slowness threshold value.
768792
*

‎lib/reporters/json-stream.js

+1
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ function clean(test) {
8585
file: test.file,
8686
duration: test.duration,
8787
currentRetry: test.currentRetry(),
88+
currentRepeat: test.currentRepeat(),
8889
speed: test.speed
8990
};
9091
}

‎lib/reporters/json.js

+1
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,7 @@ function clean(test) {
115115
file: test.file,
116116
duration: test.duration,
117117
currentRetry: test.currentRetry(),
118+
currentRepeat: test.currentRepeat(),
118119
speed: test.speed,
119120
err: cleanCycles(err)
120121
};

‎lib/runnable.js

+26
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ function Runnable(title, fn) {
4040
this._timeout = 2000;
4141
this._slow = 75;
4242
this._retries = -1;
43+
this._repeats = 1;
4344
utils.assignNewMochaID(this);
4445
Object.defineProperty(this, 'id', {
4546
get() {
@@ -60,6 +61,7 @@ utils.inherits(Runnable, EventEmitter);
6061
Runnable.prototype.reset = function () {
6162
this.timedOut = false;
6263
this._currentRetry = 0;
64+
this._currentRepeat = 1;
6365
this.pending = false;
6466
delete this.state;
6567
delete this.err;
@@ -182,6 +184,18 @@ Runnable.prototype.retries = function (n) {
182184
this._retries = n;
183185
};
184186

187+
/**
188+
* Set or get number of repeats.
189+
*
190+
* @private
191+
*/
192+
Runnable.prototype.repeats = function (n) {
193+
if (!arguments.length) {
194+
return this._repeats;
195+
}
196+
this._repeats = n;
197+
};
198+
185199
/**
186200
* Set or get current retry
187201
*
@@ -194,6 +208,18 @@ Runnable.prototype.currentRetry = function (n) {
194208
this._currentRetry = n;
195209
};
196210

211+
/**
212+
* Set or get current repeat
213+
*
214+
* @private
215+
*/
216+
Runnable.prototype.currentRepeat = function (n) {
217+
if (!arguments.length) {
218+
return this._currentRepeat;
219+
}
220+
this._currentRepeat = n;
221+
};
222+
197223
/**
198224
* Return the full title generated by recursively concatenating the parent's
199225
* full title.

‎lib/runner.js

+8
Original file line numberDiff line numberDiff line change
@@ -814,6 +814,14 @@ Runner.prototype.runTests = function (suite, fn) {
814814
self.fail(test, err);
815815
}
816816
self.emit(constants.EVENT_TEST_END, test);
817+
return self.hookUp(HOOK_TYPE_AFTER_EACH, next);
818+
} else if (test.currentRepeat() < test.repeats()) {
819+
var repeatedTest = test.clone();
820+
repeatedTest.currentRepeat(test.currentRepeat() + 1);
821+
tests.unshift(repeatedTest);
822+
823+
self.emit(constants.EVENT_TEST_RETRY, test, null);
824+
817825
return self.hookUp(HOOK_TYPE_AFTER_EACH, next);
818826
}
819827

‎lib/suite.js

+21
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ function Suite(title, parentContext, isRoot) {
7373
this.root = isRoot === true;
7474
this.pending = false;
7575
this._retries = -1;
76+
this._repeats = 1;
7677
this._beforeEach = [];
7778
this._beforeAll = [];
7879
this._afterEach = [];
@@ -127,6 +128,7 @@ Suite.prototype.clone = function () {
127128
suite.root = this.root;
128129
suite.timeout(this.timeout());
129130
suite.retries(this.retries());
131+
suite.repeats(this.repeats());
130132
suite.slow(this.slow());
131133
suite.bail(this.bail());
132134
return suite;
@@ -174,6 +176,22 @@ Suite.prototype.retries = function (n) {
174176
return this;
175177
};
176178

179+
/**
180+
* Set or get number of times to repeat a passed test.
181+
*
182+
* @private
183+
* @param {number|string} n
184+
* @return {Suite|number} for chaining
185+
*/
186+
Suite.prototype.repeats = function (n) {
187+
if (!arguments.length) {
188+
return this._repeats;
189+
}
190+
debug('repeats %d', n);
191+
this._repeats = parseInt(n, 10) || 0;
192+
return this;
193+
};
194+
177195
/**
178196
* Set or get slow `ms` or short-hand such as "2s".
179197
*
@@ -230,6 +248,7 @@ Suite.prototype._createHook = function (title, fn) {
230248
hook.parent = this;
231249
hook.timeout(this.timeout());
232250
hook.retries(this.retries());
251+
hook.repeats(this.repeats());
233252
hook.slow(this.slow());
234253
hook.ctx = this.ctx;
235254
hook.file = this.file;
@@ -344,6 +363,7 @@ Suite.prototype.addSuite = function (suite) {
344363
suite.root = false;
345364
suite.timeout(this.timeout());
346365
suite.retries(this.retries());
366+
suite.repeats(this.repeats());
347367
suite.slow(this.slow());
348368
suite.bail(this.bail());
349369
this.suites.push(suite);
@@ -362,6 +382,7 @@ Suite.prototype.addTest = function (test) {
362382
test.parent = this;
363383
test.timeout(this.timeout());
364384
test.retries(this.retries());
385+
test.repeats(this.repeats());
365386
test.slow(this.slow());
366387
test.ctx = this.ctx;
367388
this.tests.push(test);

‎lib/test.js

+3
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,9 @@ Test.prototype.clone = function () {
7373
test.timeout(this.timeout());
7474
test.slow(this.slow());
7575
test.retries(this.retries());
76+
test.repeats(this.repeats());
7677
test.currentRetry(this.currentRetry());
78+
test.currentRepeat(this.currentRepeat());
7779
test.retriedTest(this.retriedTest() || this);
7880
test.globals(this.globals());
7981
test.parent = this.parent;
@@ -91,6 +93,7 @@ Test.prototype.clone = function () {
9193
Test.prototype.serialize = function serialize() {
9294
return {
9395
$$currentRetry: this._currentRetry,
96+
$$currentRepeat: this._currentRepeat,
9497
$$fullTitle: this.fullTitle(),
9598
$$isPending: Boolean(this.pending),
9699
$$retriedTest: this._retriedTest || null,

‎test/assertions.js

+18
Original file line numberDiff line numberDiff line change
@@ -306,6 +306,24 @@ module.exports = {
306306
});
307307
}
308308
)
309+
.addAssertion(
310+
'<JSONResult> [not] to have repeated test <string>',
311+
(expect, result, title) => {
312+
expect(result.tests, '[not] to have an item satisfying', {
313+
title,
314+
currentRepeat: expect.it('to be positive')
315+
});
316+
}
317+
)
318+
.addAssertion(
319+
'<JSONResult> [not] to have repeated test <string> <number>',
320+
(expect, result, title, count) => {
321+
expect(result.tests, '[not] to have an item satisfying', {
322+
title,
323+
currentRepeat: count
324+
});
325+
}
326+
)
309327
.addAssertion(
310328
'<JSONResult> [not] to have failed with (error|errors) <any+>',
311329
function (expect, result, ...errors) {

‎test/integration/events.spec.js

+19
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,25 @@ describe('event order', function () {
5858
});
5959
});
6060

61+
describe('--repeats test case', function () {
62+
it('should assert --repeats event order', function (done) {
63+
runMochaJSON(
64+
'runner/events-repeats.fixture.js',
65+
['--repeats', '2'],
66+
function (err, res) {
67+
if (err) {
68+
done(err);
69+
return;
70+
}
71+
expect(res, 'to have passed')
72+
.and('to have failed test count', 0)
73+
.and('to have passed test count', 1);
74+
done();
75+
}
76+
);
77+
});
78+
});
79+
6180
describe('--delay test case', function () {
6281
it('should assert --delay event order', function (done) {
6382
runMochaJSON(
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
describe('repeats suite', function() {
2+
let calls = 0;
3+
this.repeats(3);
4+
5+
it('should pass', function() {
6+
7+
});
8+
9+
it('should fail on the second call', function () {
10+
calls++;
11+
console.log(`RUN: ${calls}`);
12+
if (calls > 1) throw new Error();
13+
});
14+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
'use strict';
2+
3+
describe('repeats', function () {
4+
it('should pass', () => undefined);
5+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
'use strict';
2+
3+
describe('repeats', function () {
4+
var times = 0;
5+
before(function () {
6+
console.log('before');
7+
});
8+
9+
after(function () {
10+
console.log('after');
11+
});
12+
13+
beforeEach(function () {
14+
console.log('before each', times);
15+
});
16+
17+
afterEach(function () {
18+
console.log('after each', times);
19+
});
20+
21+
it('should allow override and run appropriate hooks', function (done) {
22+
this.timeout(100);
23+
this.repeats(5);
24+
console.log('TEST', times);
25+
if (times++ > 2) {
26+
return setTimeout(done, 300);
27+
}
28+
setTimeout(done, 50);
29+
});
30+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
'use strict';
2+
describe('repeats', function () {
3+
this.repeats(2);
4+
var times = 0;
5+
6+
it('should fail on the second attempt', function () {
7+
if (times++ > 0) throw new Error;
8+
});
9+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
'use strict';
2+
3+
describe('retries', function () {
4+
var times = 0;
5+
before(function () {
6+
console.log('before');
7+
});
8+
9+
after(function () {
10+
console.log('after');
11+
});
12+
13+
beforeEach(function () {
14+
console.log('before each', times);
15+
});
16+
17+
afterEach(function () {
18+
console.log('after each', times);
19+
});
20+
21+
it('should allow override and run appropriate hooks', function () {
22+
this.retries(4);
23+
console.log('TEST', times);
24+
times++;
25+
throw new Error('retry error');
26+
});
27+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
'use strict';
2+
3+
describe('repeats', function () {
4+
this.repeats(3);
5+
describe('nested', function () {
6+
let count = 0;
7+
8+
it('should be executed only once', function () {
9+
this.repeats(1);
10+
count++;
11+
if (count > 1)
12+
throw new Error('repeat error');
13+
});
14+
});
15+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
'use strict';
2+
var Runner = require('../../../../lib/runner.js');
3+
var assert = require('assert');
4+
var constants = Runner.constants;
5+
var EVENT_HOOK_BEGIN = constants.EVENT_HOOK_BEGIN;
6+
var EVENT_HOOK_END = constants.EVENT_HOOK_END;
7+
var EVENT_RUN_BEGIN = constants.EVENT_RUN_BEGIN;
8+
var EVENT_RUN_END = constants.EVENT_RUN_END;
9+
var EVENT_SUITE_BEGIN = constants.EVENT_SUITE_BEGIN;
10+
var EVENT_SUITE_END = constants.EVENT_SUITE_END;
11+
var EVENT_TEST_BEGIN = constants.EVENT_TEST_BEGIN;
12+
var EVENT_TEST_END = constants.EVENT_TEST_END;
13+
var EVENT_TEST_PASS = constants.EVENT_TEST_PASS;
14+
var EVENT_TEST_RETRY = constants.EVENT_TEST_RETRY;
15+
16+
var emitOrder = [
17+
EVENT_RUN_BEGIN,
18+
EVENT_SUITE_BEGIN,
19+
EVENT_SUITE_BEGIN,
20+
EVENT_HOOK_BEGIN,
21+
EVENT_HOOK_END,
22+
EVENT_TEST_BEGIN,
23+
EVENT_HOOK_BEGIN,
24+
EVENT_HOOK_END,
25+
EVENT_TEST_RETRY,
26+
EVENT_HOOK_BEGIN,
27+
EVENT_HOOK_END,
28+
EVENT_TEST_BEGIN,
29+
EVENT_HOOK_BEGIN,
30+
EVENT_HOOK_END,
31+
EVENT_TEST_PASS,
32+
EVENT_TEST_END,
33+
EVENT_HOOK_BEGIN,
34+
EVENT_HOOK_END,
35+
EVENT_HOOK_BEGIN,
36+
EVENT_HOOK_END,
37+
EVENT_SUITE_END,
38+
EVENT_SUITE_END,
39+
EVENT_RUN_END
40+
];
41+
42+
var realEmit = Runner.prototype.emit;
43+
Runner.prototype.emit = function(event, ...args) {
44+
assert.strictEqual(event, emitOrder.shift());
45+
return realEmit.call(this, event, ...args);
46+
};
47+
48+
describe('suite A', function() {
49+
before('before', function() {});
50+
beforeEach('beforeEach', function() {});
51+
it('test A', () => undefined);
52+
afterEach('afterEach', function() {});
53+
after('after', function() {});
54+
});

‎test/integration/options/parallel.spec.js

+16
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,22 @@ describe('--parallel', function () {
146146
});
147147
});
148148

149+
describe('when used with --repeats', function () {
150+
it('should repeat tests appropriately', async function () {
151+
return expect(
152+
runMochaAsync('options/parallel/repeats*', ['--parallel']),
153+
'when fulfilled',
154+
'to satisfy',
155+
expect
156+
.it('to have failed')
157+
.and('to have passed test count', 1)
158+
.and('to have pending test count', 0)
159+
.and('to have failed test count', 1)
160+
.and('to contain output', /RUN: 2/)
161+
);
162+
});
163+
});
164+
149165
describe('when used with --allow-uncaught', function () {
150166
it('should bubble up an exception', async function () {
151167
return expect(
+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
'use strict';
2+
3+
var path = require('path').posix;
4+
var helpers = require('../helpers');
5+
var runMochaJSON = helpers.runMochaJSON;
6+
7+
describe('--repeats', function () {
8+
var args = [];
9+
10+
it('should repeat tests', function (done) {
11+
args = ['--repeats', '3'];
12+
var fixture = path.join('options', 'repeats');
13+
runMochaJSON(fixture, args, function (err, res) {
14+
if (err) {
15+
return done(err);
16+
}
17+
expect(res, 'to have passed')
18+
.and('not to have pending tests')
19+
.and('to have repeated test', 'should pass', 3);
20+
done();
21+
});
22+
});
23+
});

‎test/integration/repeats.spec.js

+141
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
'use strict';
2+
3+
var assert = require('assert');
4+
var helpers = require('./helpers');
5+
var runJSON = helpers.runMochaJSON;
6+
var args = [];
7+
var bang = require('../../lib/reporters/base').symbols.bang;
8+
9+
describe('repeats', function () {
10+
it('are ran in correct order', function (done) {
11+
helpers.runMocha(
12+
'repeats/hooks.fixture.js',
13+
['--reporter', 'dot'],
14+
function (err, res) {
15+
var lines, expected;
16+
17+
if (err) {
18+
done(err);
19+
return;
20+
}
21+
22+
lines = res.output
23+
.split(helpers.SPLIT_DOT_REPORTER_REGEXP)
24+
.map(function (line) {
25+
return line.trim();
26+
})
27+
.filter(function (line) {
28+
return line.length;
29+
})
30+
.slice(0, -1);
31+
32+
expected = [
33+
'before',
34+
'before each 0',
35+
'TEST 0',
36+
'after each 1',
37+
'before each 1',
38+
'TEST 1',
39+
'after each 2',
40+
'before each 2',
41+
'TEST 2',
42+
'after each 3',
43+
'before each 3',
44+
'TEST 3',
45+
'after each 4',
46+
'before each 4',
47+
'TEST 4',
48+
bang + 'after each 5',
49+
'after'
50+
];
51+
52+
expected.forEach(function (line, i) {
53+
assert.strictEqual(lines[i], line);
54+
});
55+
56+
assert.strictEqual(res.code, 1);
57+
done();
58+
}
59+
);
60+
});
61+
62+
it('should exit early if test fails', function (done) {
63+
runJSON('repeats/early-fail.fixture.js', args, function (err, res) {
64+
if (err) {
65+
return done(err);
66+
}
67+
68+
expect(res, 'to have failed')
69+
.and('to have passed test count', 0)
70+
.and('to have failed test count', 1)
71+
.and('to have repeated test', 'should fail on the second attempt', 2);
72+
73+
done();
74+
});
75+
});
76+
77+
it('should let test override', function (done) {
78+
runJSON('repeats/nested.fixture.js', args, function (err, res) {
79+
if (err) {
80+
done(err);
81+
return;
82+
}
83+
assert.strictEqual(res.stats.passes, 1);
84+
assert.strictEqual(res.stats.failures, 0);
85+
assert.strictEqual(res.stats.tests, 1);
86+
assert.strictEqual(res.tests[0].currentRepeat, 1);
87+
assert.strictEqual(res.code, 0);
88+
done();
89+
});
90+
});
91+
92+
it('should not hang w/ async test', function (done) {
93+
this.timeout(2500);
94+
helpers.runMocha(
95+
'repeats/async.fixture.js',
96+
['--reporter', 'dot'],
97+
function (err, res) {
98+
var lines, expected;
99+
100+
if (err) {
101+
done(err);
102+
return;
103+
}
104+
105+
lines = res.output
106+
.split(helpers.SPLIT_DOT_REPORTER_REGEXP)
107+
.map(function (line) {
108+
return line.trim();
109+
})
110+
.filter(function (line) {
111+
return line.length;
112+
})
113+
.slice(0, -1);
114+
115+
expected = [
116+
'before',
117+
'before each 0',
118+
'TEST 0',
119+
'after each 1',
120+
'before each 1',
121+
'TEST 1',
122+
'after each 2',
123+
'before each 2',
124+
'TEST 2',
125+
'after each 3',
126+
'before each 3',
127+
'TEST 3',
128+
bang + 'after each 4',
129+
'after'
130+
];
131+
132+
expected.forEach(function (line, i) {
133+
assert.strictEqual(lines[i], line);
134+
});
135+
136+
assert.strictEqual(res.code, 1);
137+
done();
138+
}
139+
);
140+
});
141+
});

‎test/reporters/helpers.js

+4
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,7 @@ function makeExpectedTest(
166166
expectedFile,
167167
expectedDuration,
168168
currentRetry,
169+
currentRepeat,
169170
expectedBody
170171
) {
171172
return {
@@ -178,6 +179,9 @@ function makeExpectedTest(
178179
currentRetry: function () {
179180
return currentRetry;
180181
},
182+
currentRepeat: function () {
183+
return currentRepeat;
184+
},
181185
slow: function () {}
182186
};
183187
}

‎test/reporters/json-stream.spec.js

+8
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,15 @@ describe('JSON Stream reporter', function () {
2121
var expectedFile = 'someTest.spec.js';
2222
var expectedDuration = 1000;
2323
var currentRetry = 1;
24+
var currentRepeat = 1;
2425
var expectedSpeed = 'fast';
2526
var expectedTest = makeExpectedTest(
2627
expectedTitle,
2728
expectedFullTitle,
2829
expectedFile,
2930
expectedDuration,
3031
currentRetry,
32+
currentRepeat,
3133
expectedSpeed
3234
);
3335
var expectedErrorMessage = 'error message';
@@ -78,6 +80,8 @@ describe('JSON Stream reporter', function () {
7880
expectedDuration +
7981
',"currentRetry":' +
8082
currentRetry +
83+
',"currentRepeat":' +
84+
currentRepeat +
8185
',"speed":' +
8286
`"${expectedSpeed}"` +
8387
'}]\n'
@@ -113,6 +117,8 @@ describe('JSON Stream reporter', function () {
113117
expectedDuration +
114118
',"currentRetry":' +
115119
currentRetry +
120+
',"currentRepeat":' +
121+
currentRepeat +
116122
',"speed":' +
117123
`"${expectedSpeed}"` +
118124
',"err":' +
@@ -151,6 +157,8 @@ describe('JSON Stream reporter', function () {
151157
expectedDuration +
152158
',"currentRetry":' +
153159
currentRetry +
160+
',"currentRepeat":' +
161+
currentRepeat +
154162
',"speed":' +
155163
`"${expectedSpeed}"` +
156164
',"err":' +

‎test/reporters/markdown.spec.js

+4
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ describe('Markdown reporter', function () {
7878
};
7979
var expectedDuration = 1000;
8080
var currentRetry = 1;
81+
var currentRepeat = 1;
8182
var expectedBody = 'some body';
8283
var expectedTest = {
8384
title: expectedTitle,
@@ -88,6 +89,9 @@ describe('Markdown reporter', function () {
8889
currentRetry: function () {
8990
return currentRetry;
9091
},
92+
currentRepeat: function () {
93+
return currentRepeat;
94+
},
9195
slow: noop,
9296
body: expectedBody
9397
};

‎test/unit/context.spec.js

+6
Original file line numberDiff line numberDiff line change
@@ -95,4 +95,10 @@ describe('methods', function () {
9595
expect(this.retries(), 'to be', -1);
9696
});
9797
});
98+
99+
describe('repeats', function () {
100+
it('should return the number of repeats', function () {
101+
expect(this.repeats(), 'to be', 1);
102+
});
103+
});
98104
});

‎test/unit/mocha.spec.js

+20
Original file line numberDiff line numberDiff line change
@@ -94,9 +94,11 @@ describe('Mocha', function () {
9494
mocha = sinon.createStubInstance(Mocha);
9595
mocha.timeout.returnsThis();
9696
mocha.retries.returnsThis();
97+
mocha.repeats.returnsThis();
9798
sinon.stub(Mocha.prototype, 'timeout').returnsThis();
9899
sinon.stub(Mocha.prototype, 'global').returnsThis();
99100
sinon.stub(Mocha.prototype, 'retries').returnsThis();
101+
sinon.stub(Mocha.prototype, 'repeats').returnsThis();
100102
sinon.stub(Mocha.prototype, 'rootHooks').returnsThis();
101103
sinon.stub(Mocha.prototype, 'parallelMode').returnsThis();
102104
sinon.stub(Mocha.prototype, 'globalSetup').returnsThis();
@@ -155,6 +157,24 @@ describe('Mocha', function () {
155157
});
156158
});
157159

160+
describe('when `repeats` option is present', function () {
161+
it('should attempt to set repeats`', function () {
162+
// eslint-disable-next-line no-new
163+
new Mocha({repeats: 1});
164+
expect(Mocha.prototype.repeats, 'to have a call satisfying', [1]).and(
165+
'was called once'
166+
);
167+
});
168+
});
169+
170+
describe('when `repeats` option is not present', function () {
171+
it('should not attempt to set repeats', function () {
172+
// eslint-disable-next-line no-new
173+
new Mocha({});
174+
expect(Mocha.prototype.repeats, 'was not called');
175+
});
176+
});
177+
158178
describe('when `rootHooks` option is truthy', function () {
159179
it('shouid attempt to set root hooks', function () {
160180
// eslint-disable-next-line no-new

‎test/unit/runnable.spec.js

+9
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,7 @@ describe('Runnable(title, fn)', function () {
145145
run.reset();
146146
expect(run.timedOut, 'to be false');
147147
expect(run._currentRetry, 'to be', 0);
148+
expect(run._currentRepeat, 'to be', 1);
148149
expect(run.pending, 'to be false');
149150
expect(run.err, 'to be undefined');
150151
expect(run.state, 'to be undefined');
@@ -217,6 +218,14 @@ describe('Runnable(title, fn)', function () {
217218
});
218219
});
219220

221+
describe('#repeats(n)', function () {
222+
it('should set the number of repeats', function () {
223+
var run = new Runnable();
224+
run.repeats(3);
225+
expect(run.repeats(), 'to be', 3);
226+
});
227+
});
228+
220229
describe('.run(fn)', function () {
221230
describe('when .pending', function () {
222231
it('should not invoke the callback', function (done) {

‎test/unit/runner.spec.js

+28
Original file line numberDiff line numberDiff line change
@@ -528,6 +528,34 @@ describe('Runner', function () {
528528
});
529529
});
530530

531+
it('should emit "retry" when a repeatable test passes', function (done) {
532+
var repeats = 2;
533+
var runs = 0;
534+
var retries = 0;
535+
536+
var test = new Test('i do nothing', () => {
537+
runs++;
538+
});
539+
540+
suite.repeats(repeats);
541+
suite.addTest(test);
542+
543+
runner.on(EVENT_TEST_RETRY, function (testClone, testErr) {
544+
retries++;
545+
expect(testClone.currentRepeat(), 'to be', runs);
546+
expect(testErr, 'to be', null);
547+
expect(testClone.title, 'to be', test.title);
548+
});
549+
550+
runner.run(function (failures) {
551+
expect(failures, 'to be', 0);
552+
expect(runs, 'to be', repeats);
553+
expect(retries, 'to be', repeats - 1);
554+
555+
done();
556+
});
557+
});
558+
531559
// karma-mocha is inexplicably doing this with a Hook
532560
it('should not throw an exception if something emits EVENT_TEST_END with a non-Test object', function () {
533561
expect(function () {

‎test/unit/test.spec.js

+10
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ describe('Test', function () {
1717
this._test._slow = 101;
1818
this._test._retries = 3;
1919
this._test._currentRetry = 1;
20+
this._test._repeats = 3;
21+
this._test._currentRepeat = 1;
2022
this._test._allowedGlobals = ['foo'];
2123
this._test.parent = 'foo';
2224
this._test.file = 'bar';
@@ -48,6 +50,14 @@ describe('Test', function () {
4850
expect(clone1.clone().retriedTest(), 'to be', this._test);
4951
});
5052

53+
it('should copy the repeats value', function () {
54+
expect(this._test.clone().repeats(), 'to be', 3);
55+
});
56+
57+
it('should copy the currentRepeat value', function () {
58+
expect(this._test.clone().currentRepeat(), 'to be', 1);
59+
});
60+
5161
it('should copy the globals value', function () {
5262
expect(this._test.clone().globals(), 'not to be empty');
5363
});

0 commit comments

Comments
 (0)
Please sign in to comment.