Skip to content

Commit

Permalink
test: Mock chokidar as CI filesystem seems unreliable
Browse files Browse the repository at this point in the history
  • Loading branch information
pvdlg committed Aug 29, 2017
1 parent 35610c5 commit 74cadb2
Show file tree
Hide file tree
Showing 6 changed files with 151 additions and 144 deletions.
2 changes: 1 addition & 1 deletion src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import sass from 'node-sass';
* Sass preprocessor factory.
*
* @param {Object} args Config object of custom preprocessor.
* @param {Object} [config={}] KArma's config.
* @param {Object} [config={}] Karma's config.
* @param {Object} logger Karma's logger.
* @param {Object} server Karma's server.
* @return {Function} the function to preprocess files.
Expand Down
17 changes: 10 additions & 7 deletions test/helpers/karma.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import pEvent from 'p-event';
import {Server, constants} from 'karma';
import karmaPreprocessor from '../../lib/index';
import {mockFactory} from './mock';

/**
* Base Karma configuration tu run preprocessor.
Expand All @@ -20,7 +21,6 @@ const KARMA_CONFIG = {
colors: true,
logLevel: constants.LOG_DISABLE,
browsers: ['PhantomJS'],
plugins: ['@metahub/karma-jasmine-jquery', 'karma-*', karmaPreprocessor],
};

/**
Expand All @@ -45,7 +45,7 @@ const KARMA_CONFIG = {
* @return {Promise<KarmaOutput>} A `Promise` that resolve to the Karma execution results.
*/
export async function run(files, config) {
const server = createServer(files, config, false);
const server = createServer(files, config, false, karmaPreprocessor);

server.start();
const result = await waitForRunComplete(server);
Expand All @@ -64,11 +64,12 @@ export async function run(files, config) {
* @param {Object} [config] configuration to pass to the preprocessor.
* @return {Server} The started Karma Server.
*/
export function watch(files, config) {
const server = createServer(files, config, true);
export async function watch(files, config) {
const {factory, watcher} = mockFactory(true);
const server = createServer(files, config, true, factory);

server.start();
return server;
return {server, watcher: await watcher};
}

/**
Expand All @@ -78,16 +79,18 @@ export function watch(files, config) {
* @param {Array<string>} files path of the scss/sass files and unit tests.
* @param {Object} [config] configuration to pass to the preprocessor.
* @param {boolean} autoWatch `true` for autoWatch mode, `false` for a single run.
* @param {Object} processorFactory Karma plugin factory.
* @return {Server} the configured Karma Server.
*/
function createServer(files, config, autoWatch) {
function createServer(files, config, autoWatch, processorFactory) {
return new Server(
Object.assign(KARMA_CONFIG, {
files: Array.isArray(files) ? files : [files],
sassPreprocessor: config,
customPreprocessors: {custom_sass: Object.assign({base: 'sass'}, config)},
singleRun: !autoWatch,
autoWatch,
plugins: ['@metahub/karma-jasmine-jquery', 'karma-*', processorFactory],
}),
() => 0
);
Expand All @@ -104,7 +107,7 @@ export async function waitForRunComplete(server) {
try {
const [, result] = await pEvent(server, 'run_complete', {
multiArgs: true,
timeout: 10000,
timeout: 50000,
rejectionEvents: ['browser_error'],
});

Expand Down
68 changes: 50 additions & 18 deletions test/helpers/mock.js
Original file line number Diff line number Diff line change
@@ -1,45 +1,77 @@
import {EventEmitter} from 'events';
import proxyquire from 'proxyquire';
import {spy, stub} from 'sinon';
import pify from 'pify';
import chokidar from 'chokidar';

/**
* @typedef {Object} Mock
* @typedef {Object} MockPreprocessor
* @property {Function} preprocessor The preprocessor function.
* @property {Spy} debug A spied debug log function.
* @property {Spy} error A spied error log function.
* @property {Spy} info A spied info log function.
* @property {Spy} refreshFiles A spied server's refreshFiles function.
* @property {Spy} add A spied watcher's add function.
* @property {Spy} unwatch A spied watcher's unwatch function.
* @property {FSWatcher} watcher The preprocessor local watcher.
* @property {EventEmitter} watcher The preprocessor local watcher.
* @property {Spy} watcher.add A spied watcher's add function.
* @property {Spy} watcher.unwatch A spied watcher's unwatch function.
* @property {Stub} FSWatcher A stubbed local watcher constructor.
*/

/**
* @typedef {Object} MockFactory
* @property {Oject} factory The preprocessor factory.
* @property {Promise<EventEmitter>} watcher a Promise that resolves to preprocessor local watcher.
* @property {Stub} FSWatcher A stubbed local watcher constructor.
*/

/**
* Create a mocked preprocessor factory.
*
* @method mockFactory
* @param {Boolean} autoWatch `true` for autoWatch mode, `false` for a single run.
* @return {MockFactory} mocked preprocessor factory and watcher.
*/
export function mockFactory(autoWatch) {
const FSWatcher = stub();

return {
factory: proxyquire('../../lib/index', {chokidar: {FSWatcher}}),
watcher: pify(callback => {
if (autoWatch) {
return FSWatcher.callsFake(() => {
const emitter = new EventEmitter();
const add = spy();
const unwatch = spy();

emitter.add = add;
emitter.unwatch = unwatch;
callback(null, emitter);
return emitter;
});
} else {
FSWatcher.returns(new EventEmitter());
return callback(null);
}
})(),
FSWatcher,
};
}

/**
* Create a mocked preprocessor.
*
* @method mockPreprocessor
* @param {Object} [args={}] custom preprocessor config to pass to the factory.
* @param {Object} [config={}] Karma config to pass to the factory.
* @return {Object} mocked preprocessor function and spies.
* @return {MockPreprocessor} mocked preprocessor function and spies.
*/
export default function mockPreprocessor(args = {}, config = {}) {
let add;
let unwatch;
let watcher;
const FSWatcher = stub().callsFake((...watcherArgs) => {
watcher = new chokidar.FSWatcher(...watcherArgs);
add = spy(watcher, 'add');
unwatch = spy(watcher, 'unwatch');
return watcher;
});
export async function mockPreprocessor(args = {}, config = {}) {
const debug = spy();
const error = spy();
const info = spy();
const refreshFiles = spy();
const {factory, watcher, FSWatcher} = mockFactory(config.autoWatch);
const preprocessor = pify(
proxyquire('../../lib/index', {chokidar: {FSWatcher}})['preprocessor:sass'][1](
factory['preprocessor:sass'][1](
args,
config,
{
Expand All @@ -51,5 +83,5 @@ export default function mockPreprocessor(args = {}, config = {}) {
)
);

return {preprocessor, debug, error, info, refreshFiles, add, unwatch, watcher, FSWatcher};
return {preprocessor, debug, error, info, refreshFiles, watcher: await watcher, FSWatcher};
}
18 changes: 2 additions & 16 deletions test/helpers/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,31 +14,17 @@ export function tmp(filename) {
return path.join('test/fixtures/.tmp', uuid(), filename || '');
}

/**
* Return a Promise that resolve after a delay.
*
* @method sleep
* @param {Number} delay time after which to resolve the Promise.
* @return {Promise} a Promise that resolve after a delay.
*/
export function sleep(delay) {
// eslint-disable-next-line promise/avoid-new
return new Promise(resolve => {
setTimeout(resolve, delay);
});
}

/* eslint-disable no-magic-numbers */
/**
* Return a Promise that resolve when an event is emitted and reject after a timeout expire if the event is not emitted.
*
* @method waitFor
* @param {Object} emitter object that emit events.
* @param {string} event event to listen to.
* @param {Number} [timeout=5000] maximum time to wait for the event to be emitted.
* @param {Number} [timeout=30000] maximum time to wait for the event to be emitted.
* @return {Promise} Promise tht resolve when the event is emitted.
*/
export function waitFor(emitter, event, timeout = 5000) {
export function waitFor(emitter, event, timeout = 30000) {
return pEvent(emitter, event, {timeout});
}

Expand Down
68 changes: 17 additions & 51 deletions test/integration.test.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import path from 'path';
import {utimes, copy, readFile, outputFile} from 'fs-extra';
import {copy} from 'fs-extra';
import test from 'ava';
import pTimeout from 'p-timeout';
import {stub} from 'sinon';
import {run, watch, waitForRunComplete} from './helpers/karma';
import {tmp} from './helpers/utils';
Expand All @@ -17,17 +16,23 @@ test.after(() => {
});

test('Compile scss file', async t => {
const {success, error, disconnected} = await run(['test/fixtures/basic.scss', 'test/fixtures/styles.test.js']);
const {success, error, disconnected, errMsg} = await run([
'test/fixtures/basic.scss',
'test/fixtures/styles.test.js',
]);

t.ifError(error, 'Karma returned an error');
t.ifError(error, `Karma returned the error: ${errMsg}`);
t.ifError(disconnected, 'Karma disconnected');
t.is(success, 1, 'Expected 1 test successful');
});

test('Compile scss file with custom preprocessor', async t => {
const {success, error, disconnected} = await run(['test/fixtures/basic.custom.scss', 'test/fixtures/styles.test.js']);
const {success, error, disconnected, errMsg} = await run([
'test/fixtures/basic.custom.scss',
'test/fixtures/styles.test.js',
]);

t.ifError(error, 'Karma returned an error');
t.ifError(error, `Karma returned the error: ${errMsg}`);
t.ifError(disconnected, 'Karma disconnected');
t.is(success, 1, 'Expected 1 test successful');
});
Expand All @@ -52,64 +57,25 @@ test('Re-compile scss file when dependency is modified', async t => {
copy('test/fixtures/partials/_sub-partial.scss', subPartial),
copy('test/fixtures/with-partial.scss', fixture),
]);
const server = await watch(
const {server, watcher} = await watch(
[fixture.replace('fixtures', '*').replace('with', '+(with|nomatch)'), 'test/fixtures/styles.test.js'],
{options: {includePaths: [includePath]}}
);

try {
let {success, error, disconnected} = await waitForRunComplete(server);
let {success, error, disconnected, errMsg} = await waitForRunComplete(server);

t.ifError(error, 'Karma returned an error');
t.ifError(error, `Karma returned the error: ${errMsg}`);
t.ifError(disconnected, 'Karma disconnected');
t.is(success, 1, 'Expected 1 test successful');

utimes(partial, Date.now() / 1000, Date.now() / 1000);
({success, error, disconnected} = await waitForRunComplete(server));
watcher.emit('change', partial);
({success, error, disconnected, errMsg} = await waitForRunComplete(server));

t.ifError(error, 'Karma returned an error');
t.ifError(error, `Karma returned the error: ${errMsg}`);
t.ifError(disconnected, 'Karma disconnected');
t.is(success, 1, 'Expected 1 test successful');
} finally {
await server.emitAsync('exit');
}
});

test('Do not recompile scss file when dependency is not imported anymore', async t => {
const dir = path.resolve(tmp());
const fixture = path.join(dir, 'with-partial.scss');
const includePath = path.join(dir, 'partials');
const partial = path.join(includePath, '_partial.scss');
const partialAlt = path.join(includePath, '_partial-alt.scss');
const subPartial = path.join(includePath, '_sub-partial.scss');

await Promise.all([
copy('test/fixtures/partials/_partial.scss', partial),
copy('test/fixtures/partials/_partial.scss', partialAlt),
copy('test/fixtures/partials/_sub-partial.scss', subPartial),
copy('test/fixtures/with-partial.scss', fixture),
]);
const server = await watch([fixture, 'test/fixtures/styles.test.js'], {options: {includePaths: [includePath]}});

try {
let {success, error, disconnected} = await waitForRunComplete(server);

t.ifError(error, 'Karma returned an error');
t.ifError(disconnected, 'Karma disconnected');
t.is(success, 1, 'Expected 1 test successful');

await outputFile(
fixture,
(await readFile(fixture)).toString().replace(`@import 'partial';`, `@import 'partial-alt';`)
);
({success, error, disconnected} = await waitForRunComplete(server));
t.ifError(error, 'Karma returned an error');
t.ifError(disconnected, 'Karma disconnected');
t.is(success, 1, 'Expected 1 test successful');

utimes(partial, Date.now() / 1000, Date.now() / 1000);
await t.throws(waitForRunComplete(server), pTimeout.TimeoutError);
} finally {
await server.emitAsync('exit');
}
});
Loading

0 comments on commit 74cadb2

Please sign in to comment.