Skip to content

Commit f037daf

Browse files
author
Thomas Scholtes
committed
More, improved integration tests for watching
To extend the test coverage for mocha in watch mode we add the following two tests: * Check that test files are reloaded * Check that watched dependencies are reloaded To support this change we consolidate `runMochaJSONWatchAsync`, `runMochaJSONRawAsync`, and repeated code in tests into `runMochaWatch`. We introduce `invokeMochaAsync` in `test/integration/helpers` as an async alternative to `invokeMocha`. We also eliminate the test for the cursor control character in the output. Its usefulness is dubious as it relies on an implementation detail and the other tests cover the intended behavior. We are also more explicit which test fixtures are used. Instead of setting `this.testFile` in a `beforeEach` hook we do this explicitly for the tests that require it. This prevents interference in tests that do not use the file.
1 parent e341ea4 commit f037daf

File tree

5 files changed

+168
-139
lines changed

5 files changed

+168
-139
lines changed
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
module.exports.testShouldFail = false;
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
// This will be replaced in the tests
2+
const testShouldFail = true;
3+
4+
it('checks dependency', () => {
5+
if (testShouldFail === true) {
6+
throw new Error('test failed');
7+
}
8+
});
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
const dependency = require('./lib/dependency');
2+
3+
it('checks dependency', () => {
4+
if (dependency.testShouldFail === true) {
5+
throw new Error('test failed');
6+
}
7+
});

test/integration/helpers.js

Lines changed: 33 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -113,39 +113,6 @@ module.exports = {
113113
opts
114114
);
115115
},
116-
/**
117-
* Invokes the mocha binary with the given arguments fixture using
118-
* the JSON reporter. Returns the child process and a promise for the
119-
* results of running the command. The result includes the **raw**
120-
* string output, as well as exit code.
121-
*
122-
* By default, `STDERR` is ignored. Pass `{stdio: 'pipe'}` as `opts` if you
123-
* want it as part of the result output.
124-
*
125-
* @param {string[]} args - Array of args
126-
* @param {Object} [opts] - Opts for `spawn()`
127-
* @returns {[ChildProcess|Promise<Result>]}
128-
*/
129-
runMochaJSONRawAsync: function(args, opts) {
130-
args = args || [];
131-
132-
let childProcess;
133-
const resultPromise = new Promise((resolve, reject) => {
134-
childProcess = invokeSubMocha(
135-
[...args, '--reporter', 'json'],
136-
function(err, resRaw) {
137-
if (err) {
138-
reject(err);
139-
} else {
140-
resolve(resRaw);
141-
}
142-
},
143-
opts
144-
);
145-
});
146-
147-
return [childProcess, resultPromise];
148-
},
149116

150117
/**
151118
* regular expression used for splitting lines based on new line / dot symbol.
@@ -174,6 +141,8 @@ module.exports = {
174141
*/
175142
invokeMocha: invokeMocha,
176143

144+
invokeMochaAsync: invokeMochaAsync,
145+
177146
/**
178147
* Resolves the path to a fixture to the full path.
179148
*/
@@ -227,6 +196,37 @@ function invokeMocha(args, fn, opts) {
227196
);
228197
}
229198

199+
/**
200+
* Invokes the mocha binary with the given arguments. Returns the
201+
* child process and a promise for the results of running the
202+
* command. The promise resolves when the child process exits. The
203+
* result includes the **raw** string output, as well as exit code.
204+
*
205+
* By default, `STDERR` is ignored. Pass `{stdio: 'pipe'}` as `opts` if you
206+
* want it as part of the result output.
207+
*
208+
* @param {string[]} args - Array of args
209+
* @param {Object} [opts] - Opts for `spawn()`
210+
* @returns {[ChildProcess|Promise<Result>]}
211+
*/
212+
function invokeMochaAsync(args, opts) {
213+
let mochaProcess;
214+
const resultPromise = new Promise((resolve, reject) => {
215+
mochaProcess = _spawnMochaWithListeners(
216+
defaultArgs([MOCHA_EXECUTABLE].concat(args)),
217+
(err, result) => {
218+
if (err) {
219+
reject(err);
220+
} else {
221+
resolve(result);
222+
}
223+
},
224+
opts
225+
);
226+
});
227+
return [mochaProcess, resultPromise];
228+
}
229+
230230
function invokeSubMocha(args, fn, opts) {
231231
if (typeof args === 'function') {
232232
opts = fn;

test/integration/options/watch.spec.js

Lines changed: 119 additions & 106 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,6 @@ const fs = require('fs-extra');
44
const os = require('os');
55
const path = require('path');
66
const helpers = require('../helpers');
7-
const runMochaJSONRawAsync = helpers.runMochaJSONRawAsync;
8-
9-
const sigintExitCode = 130;
107

118
describe('--watch', function() {
129
describe('when enabled', function() {
@@ -15,11 +12,6 @@ describe('--watch', function() {
1512

1613
beforeEach(function() {
1714
this.tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'mocha-'));
18-
19-
const fixtureSource = helpers.DEFAULT_FIXTURE;
20-
21-
this.testFile = path.join(this.tempDir, 'test.js');
22-
fs.copySync(fixtureSource, this.testFile);
2315
});
2416

2517
afterEach(function() {
@@ -28,79 +20,39 @@ describe('--watch', function() {
2820
}
2921
});
3022

31-
it('should show the cursor and signal correct exit code, when watch process is terminated', function() {
32-
// Feature works but SIMULATING the signal (ctrl+c) via child process
33-
// does not work due to lack of POSIX signal compliance on Windows.
34-
if (process.platform === 'win32') {
35-
this.skip();
36-
}
37-
38-
const [mocha, resultPromise] = runMochaJSONRawAsync([
39-
helpers.DEFAULT_FIXTURE,
40-
'--watch'
41-
]);
42-
43-
return sleep(1000)
44-
.then(() => {
45-
mocha.kill('SIGINT');
46-
return resultPromise;
47-
})
48-
.then(data => {
49-
const expectedCloseCursor = '\u001b[?25h';
50-
expect(data.output, 'to contain', expectedCloseCursor);
51-
52-
expect(data.code, 'to be', sigintExitCode);
53-
});
54-
});
55-
5623
it('reruns test when watched test file is touched', function() {
57-
const [mocha, outputPromise] = runMochaJSONWatchAsync([this.testFile], {
58-
cwd: this.tempDir
59-
});
24+
const testFile = path.join(this.tempDir, 'test.js');
25+
copyFixture('__default__', testFile);
6026

61-
return expect(
62-
sleep(1000)
63-
.then(() => {
64-
touchFile(this.testFile);
65-
return sleep(1000);
66-
})
67-
.then(() => {
68-
mocha.kill('SIGINT');
69-
return outputPromise;
70-
}),
71-
'when fulfilled',
72-
'to have length',
73-
2
74-
);
27+
return runMochaWatch([testFile], this.tempDir, () => {
28+
touchFile(testFile);
29+
}).then(results => {
30+
expect(results, 'to have length', 2);
31+
});
7532
});
7633

7734
it('reruns test when file matching extension is touched', function() {
35+
const testFile = path.join(this.tempDir, 'test.js');
36+
copyFixture('__default__', testFile);
37+
7838
const watchedFile = path.join(this.tempDir, 'file.xyz');
7939
touchFile(watchedFile);
80-
const [mocha, outputPromise] = runMochaJSONWatchAsync(
81-
[this.testFile, '--extension', 'xyz,js'],
82-
{
83-
cwd: this.tempDir
84-
}
85-
);
8640

87-
return expect(
88-
sleep(1000)
89-
.then(() => {
90-
touchFile(watchedFile);
91-
return sleep(1000);
92-
})
93-
.then(() => {
94-
mocha.kill('SIGINT');
95-
return outputPromise;
96-
}),
97-
'when fulfilled',
98-
'to have length',
99-
2
100-
);
41+
return runMochaWatch(
42+
[testFile, '--extension', 'xyz,js'],
43+
this.tempDir,
44+
() => {
45+
touchFile(watchedFile);
46+
}
47+
).then(results => {
48+
expect(results, 'to have length', 2);
49+
});
10150
});
10251

103-
it('ignores files in "node_modules" and ".git"', function() {
52+
it('ignores files in "node_modules" and ".git" by default', function() {
53+
const testFile = path.join(this.tempDir, 'test.js');
54+
copyFixture('__default__', testFile);
55+
10456
const nodeModulesFile = path.join(
10557
this.tempDir,
10658
'node_modules',
@@ -111,50 +63,91 @@ describe('--watch', function() {
11163
touchFile(gitFile);
11264
touchFile(nodeModulesFile);
11365

114-
const [mocha, outputPromise] = runMochaJSONWatchAsync(
115-
[this.testFile, '--extension', 'xyz,js'],
116-
{
117-
cwd: this.tempDir
66+
return runMochaWatch(
67+
[testFile, '--extension', 'xyz,js'],
68+
this.tempDir,
69+
() => {
70+
touchFile(gitFile);
71+
touchFile(nodeModulesFile);
11872
}
119-
);
73+
).then(results => {
74+
expect(results, 'to have length', 1);
75+
});
76+
});
12077

121-
return expect(
122-
sleep(1000)
123-
.then(() => {
124-
touchFile(gitFile);
125-
touchFile(nodeModulesFile);
126-
})
127-
.then(() => sleep(1000))
128-
.then(() => {
129-
mocha.kill('SIGINT');
130-
return outputPromise;
131-
}),
132-
'when fulfilled',
133-
'to have length',
134-
1
135-
);
78+
it('reloads test files when they change', function() {
79+
const testFile = path.join(this.tempDir, 'test.js');
80+
copyFixture('options/watch/test-file-change', testFile);
81+
82+
return runMochaWatch([testFile], this.tempDir, () => {
83+
replaceFileContents(
84+
testFile,
85+
'testShouldFail = true',
86+
'testShouldFail = false'
87+
);
88+
}).then(results => {
89+
expect(results, 'to have length', 2);
90+
expect(results[0].passes, 'to have length', 0);
91+
expect(results[0].failures, 'to have length', 1);
92+
expect(results[1].passes, 'to have length', 1);
93+
expect(results[1].failures, 'to have length', 0);
94+
});
95+
});
96+
97+
it('reloads test dependencies when they change', function() {
98+
const testFile = path.join(this.tempDir, 'test.js');
99+
copyFixture('options/watch/test-with-dependency', testFile);
100+
101+
const dependency = path.join(this.tempDir, 'lib', 'dependency.js');
102+
copyFixture('options/watch/dependency', dependency);
103+
104+
return runMochaWatch([testFile], this.tempDir, () => {
105+
replaceFileContents(
106+
dependency,
107+
'module.exports.testShouldFail = false',
108+
'module.exports.testShouldFail = true'
109+
);
110+
}).then(results => {
111+
expect(results, 'to have length', 2);
112+
expect(results[0].passes, 'to have length', 1);
113+
expect(results[0].failures, 'to have length', 0);
114+
expect(results[1].passes, 'to have length', 0);
115+
expect(results[1].failures, 'to have length', 1);
116+
});
136117
});
137118
});
138119
});
139120

140121
/**
141-
* Invokes the mocha binary with the `--watch` argument for the given fixture.
122+
* Runs the mocha binary in watch mode calls `change` and returns the
123+
* JSON reporter output.
142124
*
143-
* Returns child process and a promise for the test results. The test results
144-
* are an array of JSON objects generated by the JSON reporter.
125+
* The function starts mocha with the given arguments and `--watch` and
126+
* waits until the first test run has completed. Then it calls `change`
127+
* and waits until the second test run has been completed. Mocha is
128+
* killed and the list of JSON outputs is returned.
145129
*/
146-
function runMochaJSONWatchAsync(args, spawnOpts) {
147-
args = [...args, '--watch'];
148-
const [mocha, mochaDone] = runMochaJSONRawAsync(args, spawnOpts);
149-
const testResults = mochaDone.then(data => {
150-
const testResults = data.output
151-
// eslint-disable-next-line no-control-regex
152-
.replace(/\u001b\[\?25./g, '')
153-
.split('\u001b[2K')
154-
.map(x => JSON.parse(x));
155-
return testResults;
156-
});
157-
return [mocha, testResults];
130+
function runMochaWatch(args, cwd, change) {
131+
const [mochaProcess, resultPromise] = helpers.invokeMochaAsync(
132+
[...args, '--watch', '--reporter', 'json'],
133+
{cwd}
134+
);
135+
136+
return sleep(1000)
137+
.then(() => change())
138+
.then(() => sleep(1000))
139+
.then(() => {
140+
mochaProcess.kill('SIGINT');
141+
return resultPromise;
142+
})
143+
.then(data => {
144+
const testResults = data.output
145+
// eslint-disable-next-line no-control-regex
146+
.replace(/\u001b\[\?25./g, '')
147+
.split('\u001b[2K')
148+
.map(x => JSON.parse(x));
149+
return testResults;
150+
});
158151
}
159152

160153
/**
@@ -166,6 +159,26 @@ function touchFile(file) {
166159
fs.appendFileSync(file, ' ');
167160
}
168161

162+
/**
163+
* Synchronously eplace all substrings matched by `pattern` with
164+
* `replacement` in the file’s content.
165+
*/
166+
function replaceFileContents(file, pattern, replacement) {
167+
const contents = fs.readFileSync(file, 'utf-8');
168+
const newContents = contents.replace(pattern, replacement);
169+
fs.writeFileSync(file, newContents, 'utf-8');
170+
}
171+
172+
/**
173+
* Synchronously copy a fixture to the given destion file path. Creates
174+
* parent directories of the destination path if necessary.
175+
*/
176+
function copyFixture(fixtureName, dest) {
177+
const fixtureSource = helpers.resolveFixturePath(fixtureName);
178+
fs.ensureDirSync(path.dirname(dest));
179+
fs.copySync(fixtureSource, dest);
180+
}
181+
169182
function sleep(time) {
170183
return new Promise(resolve => {
171184
setTimeout(resolve, time);

0 commit comments

Comments
 (0)