fluidtrends/savor

View on GitHub
src/index.ts

Summary

Maintainability
A
0 mins
Test Coverage
import chai from 'chai'
import chaiAsPromised from 'chai-as-promised'
import sinon from 'sinon'
import path from 'path'
import fs from 'fs-extra'
import tmp from 'tmp'
import {
    Context,
    ResultError,
    Result,
    Completion
} from './types'

chai.use(chaiAsPromised);

var tests: any[] = [];
var cwdDir = process.cwd();
var testDir = path.join(cwdDir, 'test');
var srcDir = path.join(cwdDir, 'lib');

function addTest(name: string, exec?: any) {
    tests.push({
        name: name,
        exec: exec
    });
}

function beforeEach(test: string, context: Context, done: Completion) {
    context.dir = tmp.dirSync().name;
    process.chdir(context.dir);
    done && done();
}

function promiseShouldFail(promise: Promise<any>, done: Completion, handler: ResultError) {
    promise.then(() => done(new Error('Promise expected to fail'))).
    catch((error: Error) => {
        handler(error)
        done()
    }).
    catch((error: Error) => done(error))
}

function promiseShouldSucceed(promise: Promise<any>, done: Completion, handler: Result) {
    promise.then((result: any) => {
        handler(result)
        done()
    }).
    catch((error: Error) => done(error))
}

function afterEach(test: any, context: Context) {
    fs.removeSync(context.dir);
}

function runTest(test: any, done: Completion) {
    beforeEach(test, savor.context, function() {
        // Run the actual test and give the test all the tools it needs for optimal execution
        test.exec(savor.context, function(error: Error) {
            // Clean up the context
            afterEach(test, savor.context);

            // Finish up the test
            done && done(error)
        });
    });
}

function runAllTests(name: any, allDone?: any) {

    // We're going to create a single, flat list of scenarios
    describe(name, function() {

        after(function() {
            // Tell the caller when all tests have finished running
            tmp.setGracefulCleanup();
            allDone && allDone();
        });

        tests.forEach(function(test) {
            // Have a look at all tests and define each test,
            // and pass it all the required dependencies to enable it
            // to perform work without worrying about dependencies
            it(test.name, function(done) {
                // Increase the timeout so that we can handle slower CI cycles if necessary
                this.timeout(60000);

                // Execute the actual test
                runTest(test, done);
            });
        });

        // Clean up the test fixture
        tests = [];
    });
}

function copyAssetToContext(src: any, dest: any, context: Context) {
    var sourceAsset = path.resolve(testDir, src);
    var targetAsset = path.resolve(context.dir, dest);

    if (!fs.existsSync(sourceAsset)) {
        // The source asset requested is missing
        throw new Error("The test asset is missing");
    }

    // Attempt to copy the asset
    fs.copySync(sourceAsset, targetAsset);
    
    if (!fs.existsSync(targetAsset)) {
        // The target asset was not copied successfully
        throw new Error("The test asset could not be copied");
    }
}

const savor = {
    context: {
        expect: chai.expect,
        assert: chai.assert,
        stub: sinon.stub,
        replace: sinon.replace,
        replaceGetter: sinon.replaceGetter,
        replaceSetter: sinon.replaceSetter,
        spy: sinon.spy,
        dir: process.cwd(),
        clock: sinon.useFakeTimers
    } as Context,
    src: function(name: string) {
        return require(path.join(srcDir, name));
    },
    capture: function(stream: any) {
        var original = stream.write;
        var content = '';

        stream.write = function(string: string) {
            // Keep track of the stream's data
            content += string.toString();
            return true;
        }

        return {
            release: () => {
                stream.write = original;
                return content;
            }
        }
    },
    add: function(name: any, exec?: any) {
        addTest(name, exec);
        return savor;
    },
    run: function(name: any, done?: Completion) {
        runAllTests(name, done);
    },
    promiseShouldSucceed: promiseShouldSucceed,
    promiseShouldFail: promiseShouldFail,
    allTests: tests,
    reset: function() {
        tests = []
    },
    addAsset: function(src: any, dest: any, context: any) {
        copyAssetToContext(src, dest, context);
    }
}

export * from './types'
export default savor