aymericbeaumet/metalsmith-concat

View on GitHub
test.js

Summary

Maintainability
F
1 wk
Test Coverage
const path = require('path');
const test = require('ava');
const sinon = require('sinon');
const metalsmithConcat = require('.');

function filesFixture() {
    return {
        'first/file': {contents: Buffer.from('lorem')},
        'second/file': {contents: Buffer.from(' ')},
        'third/file': {contents: Buffer.from('ipsum')}
    };
}

function metalsmithFixture() {
    return {
        _directory: path.resolve(__dirname, 'fixture')
    };
}

test.afterEach.always(() => {
    sinon.restore();
});

test.cb('metalsmith-concat should concatenate all files by default', t => {
    t.plan(2);
    const files = filesFixture();
    const plugin = metalsmithConcat({output: 'output/path'});
    plugin(files, metalsmithFixture(), error => {
        t.falsy(error);
        t.deepEqual(files, {
            'output/path': {contents: Buffer.from('lorem\n \nipsum\n')}
        });
        t.end();
    });
});

test.cb(
    'metalsmith-concat should only concatenate the specified files if options.files is passed as a string',
    t => {
        t.plan(2);
        const files = filesFixture();
        const plugin = metalsmithConcat({files: 'first/*', output: 'output/path'});
        plugin(files, metalsmithFixture(), error => {
            t.falsy(error);
            t.deepEqual(files, {
                'output/path': {contents: Buffer.from('lorem\n')},
                'second/file': {contents: Buffer.from(' ')},
                'third/file': {contents: Buffer.from('ipsum')}
            });
            t.end();
        });
    }
);

test.cb(
    'metalsmith-concat should only concatenate the specified files if options.files is passed as a strings array',
    t => {
        t.plan(2);
        const files = filesFixture();
        const plugin = metalsmithConcat({
            files: ['first/*', 'third/*'],
            output: 'output/path'
        });
        plugin(files, metalsmithFixture(), error => {
            t.falsy(error);
            t.deepEqual(files, {
                'output/path': {contents: Buffer.from('lorem\nipsum\n')},
                'second/file': {contents: Buffer.from(' ')}
            });
            t.end();
        });
    }
);

test.cb(
    'metalsmith-concat should concatenate no files if the matching pattern is an empty string',
    t => {
        t.plan(2);
        const files = filesFixture();
        const plugin = metalsmithConcat({files: '', output: 'output/path'});
        plugin(files, metalsmithFixture(), error => {
            t.falsy(error);
            t.deepEqual(files, {
                'output/path': {contents: Buffer.from('')},
                'first/file': {contents: Buffer.from('lorem')},
                'second/file': {contents: Buffer.from(' ')},
                'third/file': {contents: Buffer.from('ipsum')}
            });
            t.end();
        });
    }
);

test.cb(
    'metalsmith-concat should concatenate no files if the matching pattern is an empty array',
    t => {
        t.plan(2);
        const files = filesFixture();
        const plugin = metalsmithConcat({files: [], output: 'output/path'});
        plugin(files, metalsmithFixture(), error => {
            t.falsy(error);
            t.deepEqual(files, {
                'output/path': {contents: Buffer.from('')},
                'first/file': {contents: Buffer.from('lorem')},
                'second/file': {contents: Buffer.from(' ')},
                'third/file': {contents: Buffer.from('ipsum')}
            });
            t.end();
        });
    }
);

test.cb(
    'metalsmith-concat should concatenate file in the order of the files keys',
    t => {
        t.plan(2);
        const files = filesFixture();
        const plugin = metalsmithConcat({output: 'output/path'});
        plugin(files, metalsmithFixture(), error => {
            t.falsy(error);
            t.deepEqual(files, {
                'output/path': {contents: Buffer.from('lorem\n \nipsum\n')}
            });
            t.end();
        });
    }
);

test.cb(
    'metalsmith-concat should concatenate file in the order of the files keys / options.files patterns',
    t => {
        t.plan(2);
        const files = filesFixture();
        const plugin = metalsmithConcat({
            files: ['third/*', 'second/*', 'first/*'],
            output: 'output/path'
        });
        plugin(files, metalsmithFixture(), error => {
            t.falsy(error);
            t.deepEqual(files, {
                'output/path': {contents: Buffer.from('ipsum\n \nlorem\n')}
            });
            t.end();
        });
    }
);

test.cb(
    'metalsmith-concat should insert the new line as `\\n` by default',
    t => {
        t.plan(2);
        const files = filesFixture();
        const plugin = metalsmithConcat({files: 'first/*', output: 'output/path'});
        plugin(files, metalsmithFixture(), error => {
            t.falsy(error);
            t.deepEqual(files, {
                'output/path': {contents: Buffer.from('lorem\n')},
                'second/file': {contents: Buffer.from(' ')},
                'third/file': {contents: Buffer.from('ipsum')}
            });
            t.end();
        });
    }
);

test.cb(
    'metalsmith-concat should insert the new line as `\\n` if options.insertNewline is true',
    t => {
        t.plan(2);
        const files = filesFixture();
        const plugin = metalsmithConcat({
            files: 'first/*',
            output: 'output/path',
            insertNewline: true
        });
        plugin(files, metalsmithFixture(), error => {
            t.falsy(error);
            t.deepEqual(files, {
                'output/path': {contents: Buffer.from('lorem\n')},
                'second/file': {contents: Buffer.from(' ')},
                'third/file': {contents: Buffer.from('ipsum')}
            });
            t.end();
        });
    }
);

test.cb(
    'metalsmith-concat should not insert the new line if options.insertNewline is false',
    t => {
        t.plan(2);
        const files = filesFixture();
        const plugin = metalsmithConcat({
            files: 'first/*',
            output: 'output/path',
            insertNewline: false
        });
        plugin(files, metalsmithFixture(), error => {
            t.falsy(error);
            t.deepEqual(files, {
                'output/path': {contents: Buffer.from('lorem')},
                'second/file': {contents: Buffer.from(' ')},
                'third/file': {contents: Buffer.from('ipsum')}
            });
            t.end();
        });
    }
);

test.cb(
    'metalsmith-concat should support custom new line if options.insertNewline is a string',
    t => {
        t.plan(2);
        const files = filesFixture();
        const plugin = metalsmithConcat({
            files: 'first/*',
            output: 'output/path',
            insertNewline: '\r\n'
        });
        plugin(files, metalsmithFixture(), error => {
            t.falsy(error);
            t.deepEqual(files, {
                'output/path': {contents: Buffer.from('lorem\r\n')},
                'second/file': {contents: Buffer.from(' ')},
                'third/file': {contents: Buffer.from('ipsum')}
            });
            t.end();
        });
    }
);

test.cb('metalsmith-concat should delete the source files by default', t => {
    t.plan(2);
    const files = filesFixture();
    const plugin = metalsmithConcat({output: 'output/path'});
    plugin(files, metalsmithFixture(), error => {
        t.falsy(error);
        t.deepEqual(files, {
            'output/path': {contents: Buffer.from('lorem\n \nipsum\n')}
        });
        t.end();
    });
});

test.cb(
    'metalsmith-concat should delete the concatenated original files if options.keepConcatenated is false',
    t => {
        t.plan(2);
        const files = filesFixture();
        const plugin = metalsmithConcat({
            output: 'output/path',
            keepConcatenated: false
        });
        plugin(files, metalsmithFixture(), error => {
            t.falsy(error);
            t.deepEqual(files, {
                'output/path': {contents: Buffer.from('lorem\n \nipsum\n')}
            });
            t.end();
        });
    }
);

test.cb(
    'metalsmith-concat should keep the concatenated original files if options.keepConcatenated is true',
    t => {
        t.plan(2);
        const files = filesFixture();
        const plugin = metalsmithConcat({
            output: 'output/path',
            keepConcatenated: true
        });
        plugin(files, metalsmithFixture(), error => {
            t.falsy(error);
            t.deepEqual(files, {
                'output/path': {contents: Buffer.from('lorem\n \nipsum\n')},
                'first/file': {contents: Buffer.from('lorem')},
                'second/file': {contents: Buffer.from(' ')},
                'third/file': {contents: Buffer.from('ipsum')}
            });
            t.end();
        });
    }
);

test.cb('metalsmith-concat should throw if no options object is passed', t => {
    t.plan(2);
    try {
        metalsmithConcat();
    } catch (error) {
        t.true(error instanceof Error);
        t.is(
            error.message,
            '`options.output` is mandatory and has to be a non-empty string'
        );
        t.end();
    }
});

test.cb(
    'metalsmith-concat should throw if options.output is not defined',
    t => {
        t.plan(2);
        try {
            metalsmithConcat({});
        } catch (error) {
            t.true(error instanceof Error);
            t.is(
                error.message,
                '`options.output` is mandatory and has to be a non-empty string'
            );
            t.end();
        }
    }
);

test.cb(
    'metalsmith-concat should throw error if options.output is not a string',
    t => {
        t.plan(2);
        try {
            metalsmithConcat({output: false});
        } catch (error) {
            t.true(error instanceof Error);
            t.is(
                error.message,
                '`options.output` is mandatory and has to be a non-empty string'
            );
            t.end();
        }
    }
);

test.cb(
    'metalsmith-concat should throw error if options.output is an empty string',
    t => {
        t.plan(2);
        try {
            metalsmithConcat({output: ''});
        } catch (error) {
            t.true(error instanceof Error);
            t.is(
                error.message,
                '`options.output` is mandatory and has to be a non-empty string'
            );
            t.end();
        }
    }
);

test.cb(
    'metalsmith-concat should return an error if options.output already exists by default',
    t => {
        t.plan(2);
        const plugin = metalsmithConcat({output: 'output/path'});
        plugin({'output/path': {}}, metalsmithFixture(), error => {
            t.true(error instanceof Error);
            t.is(
                error.message,
                'The file "output/path" already exists, see the documentation for \'options.forceOutput\''
            );
            t.end();
        });
    }
);

test.cb(
    'metalsmith-concat should return an error if options.output already exists when indicated as option',
    t => {
        t.plan(2);
        const plugin = metalsmithConcat({
            output: 'output/path',
            forceOutput: false
        });
        plugin({'output/path': {}}, metalsmithFixture(), error => {
            t.true(error instanceof Error);
            t.is(
                error.message,
                'The file "output/path" already exists, see the documentation for \'options.forceOutput\''
            );
            t.end();
        });
    }
);

test.cb(
    'metalsmith-concat should override existing file check if forceOutput is enabled',
    t => {
        t.plan(2);
        const files = {
            'output/path1': {contents: Buffer.from('test123')},
            'output/path2': {contents: Buffer.from('456test')}
        };
        const plugin = metalsmithConcat({
            files: ['output/path1', 'output/path2'],
            output: 'output/path',
            forceOutput: true
        });
        plugin(files, metalsmithFixture(), error => {
            t.falsy(error);
            t.deepEqual(files, {
                'output/path': {contents: Buffer.from('test123\n456test\n')}
            });
            t.end();
        });
    }
);

// https://github.com/aymericbeaumet/metalsmith-concat/issues/9
test.cb(
    'metalsmith-concat should not concatenate already concatenated files by default',
    t => {
        t.plan(2);
        const files = {
            'js/jquery.js': {contents: Buffer.from('jquery')},
            'js/bootstrap/1.js': {contents: Buffer.from('bootstrap1')},
            'js/bootstrap/2.js': {contents: Buffer.from('bootstrap2')},
            'js/other/1.js': {contents: Buffer.from('other1')},
            'js/other/2.js': {contents: Buffer.from('other2')}
        };
        const plugin = metalsmithConcat({
            files: ['js/jquery.js', 'js/bootstrap/*.js', 'js/**/*.js'],
            output: 'js/app.js'
        });
        plugin(files, metalsmithFixture(), error => {
            t.falsy(error);
            t.deepEqual(files, {
                'js/app.js': {
                    contents: Buffer.from(
                        'jquery\nbootstrap1\nbootstrap2\nother1\nother2\n'
                    )
                }
            });
            t.end();
        });
    }
);

// https://github.com/aymericbeaumet/metalsmith-concat/issues/8
test.cb(
    'metalsmith-concat should normalize options.output and options.files to be cross-platform compatible',
    t => {
        t.plan(2);
        const files = {
            'js/jquery.js': {contents: Buffer.from('jquery')}
        };
        const plugin = metalsmithConcat({
            files: ['js\\jquery.js'],
            output: 'js\\app.js'
        });
        plugin(files, metalsmithFixture(), error => {
            t.falsy(error);
            t.deepEqual(files, {
                'js/app.js': {contents: Buffer.from('jquery\n')}
            });
            t.end();
        });
    }
);

test.cb(
    'metalsmith-concat should not include files from options.searchPaths by default',
    t => {
        t.plan(2);
        const files = {};
        const plugin = metalsmithConcat({files: ['**/*.md'], output: 'output'});
        plugin(files, metalsmithFixture(), error => {
            t.falsy(error);
            t.true(files.output.contents.equals(Buffer.from('')));
            t.end();
        });
    }
);

test.cb(
    'metalsmith-concat should resolve relative searchPaths from the project root (as a string)',
    t => {
        t.plan(2);
        const files = {};
        const plugin = metalsmithConcat({
            files: ['**/*.md'],
            output: 'output',
            searchPaths: '.'
        });
        plugin(files, metalsmithFixture(), error => {
            t.falsy(error);
            t.true(
                files.output.contents.equals(Buffer.from('anotherdir\n\nroot\n\n'))
            );
            t.end();
        });
    }
);

test.cb(
    'metalsmith-concat should resolve relative searchPaths from the project root (as an array of strings)',
    t => {
        t.plan(2);
        const files = {};
        const plugin = metalsmithConcat({
            files: ['**/*.md'],
            output: 'output',
            searchPaths: ['.']
        });
        plugin(files, metalsmithFixture(), error => {
            t.falsy(error);
            t.true(
                files.output.contents.equals(Buffer.from('anotherdir\n\nroot\n\n'))
            );
            t.end();
        });
    }
);

// Serial because of mocking
test.serial.cb('metalsmith-concat should forward glob errors', t => {
    t.plan(2);
    sinon.replace(require('glob'), 'glob', (_pattern, _options, callback) =>
        callback(new Error('glob.glob error'))
    );
    const files = {};
    const plugin = metalsmithConcat({
        files: ['**/*.md'],
        output: 'output',
        searchPaths: ['.']
    });
    plugin(files, metalsmithFixture(), error => {
        t.true(error instanceof Error);
        t.is(error.message, 'glob.glob error');
        t.end();
    });
});

// Serial because of mocking
test.serial.cb('metalsmith-concat should forward fs.readFile errors', t => {
    t.plan(2);
    sinon.replace(require('fs'), 'readFile', (_filepath, callback) =>
        callback(new Error('fs.readFile error'))
    );
    const files = {};
    const plugin = metalsmithConcat({
        files: ['**/*.md'],
        output: 'output',
        searchPaths: ['.']
    });
    plugin(files, metalsmithFixture(), error => {
        t.true(error instanceof Error);
        t.is(error.message, 'fs.readFile error');
        t.end();
    });
});

// https://github.com/aymericbeaumet/metalsmith-concat/issues/25#issue-145523237
test.cb(
    'metalsmith-concat should respect the order given by the metalsmith files object',
    t => {
        t.plan(2);
        const files = {
            '01_foo.css': {contents: Buffer.from('first')},
            '02_bar.css': {contents: Buffer.from('second')}
        };
        const plugin = metalsmithConcat({output: 'output/path'});
        plugin(files, metalsmithFixture(), error => {
            t.falsy(error);
            t.deepEqual(files, {
                'output/path': {contents: Buffer.from('first\nsecond\n')}
            });
            t.end();
        });
    }
);

// https://github.com/aymericbeaumet/metalsmith-concat/issues/26
test.cb(
    'metalsmith-concat should simplify the given path in the pattern',
    t => {
        t.plan(2);
        const files = {
            'foo/bar/baz': {contents: Buffer.from('foobarbaz')}
        };
        const plugin = metalsmithConcat({
            files: 'foo//\\\\bar\\\\//baz',
            output: 'output/path'
        });
        plugin(files, metalsmithFixture(), error => {
            t.falsy(error);
            t.deepEqual(files, {
                'output/path': {contents: Buffer.from('foobarbaz\n')}
            });
            t.end();
        });
    }
);