@retailmenot/roux
Version:
Roux SDK and specification
1,692 lines (1,518 loc) • 38 kB
JavaScript
;
/* eslint camelcase: [2, {properties: "never"}], quote-props: 0, max-len: [1, 200] */
var mockfs = require('mock-fs');
var _ = require('lodash');
var mockery = require('mockery');
var path = require('path');
var Promise = require('bluebird');
var sinon = require('sinon');
var tap = require('tap');
var roux = require('../');
var Ingredient = require('../lib/ingredient');
var Pantry = require('../lib/pantry');
var initialize = roux.initialize;
var parseIngredientPath = roux.parseIngredientPath;
var resolve = roux.resolve;
var normalizeConfig = roux.normalizeConfig;
var pantryErrors = roux.errors;
function restoreMockFS() {
mockfs.restore();
}
tap.test('API', function (t) {
t.autoend();
t.type(
roux.Pantry,
'function',
'exports the Pantry class'
);
t.type(
roux.Ingredient,
'function',
'exports the Ingredient class'
);
t.type(
initialize,
'function',
'exports an initialize method'
);
t.test('initialize', function (t) {
t.autoend();
t.test('returns a Promise', function (t) {
mockfs({
'empty-pantry': {
}
});
var promise = initialize({
path: path.resolve('empty-pantry'),
name: 'empty-pantry'
});
t.type(promise, 'object');
t.type(promise.then, 'function');
t.test('resolved with Pantry instance on success', function (t) {
initialize(
{
path: path.resolve('empty-pantry'),
name: 'empty-pantry'
})
.then(function (pantry) {
t.type(pantry, Pantry);
t.end();
});
});
t.test('rejected with error on failure', function (t) {
initialize(
{
path: path.resolve('no-such-pantry'),
name: 'empty-pantry'
})
.catch(function (error) {
t.type(error, Error);
t.end();
});
});
restoreMockFS();
t.end();
});
t.test('accepts a required config object', function (t) {
t.autoend();
t.throws(function () {
initialize();
}, 'throws if config is not passed');
t.throws(function () {
initialize(123);
}, 'throws if config not an object');
t.throws(function () {
initialize(true);
}, 'throws if config not an object');
t.test('config.predicates', function (t) {
t.autoend();
t.test('can be regexes', function (t) {
mockfs({
'entrypoints-pantry': {
'custom-only': {
'ingredient.md': '',
'index.foo': ''
}
}
});
return initialize(
{
path: path.resolve('entrypoints-pantry'),
name: 'entrypoints-pantry',
predicates: {
foo: /^index.foo$/,
bar: /^index.bar$/
}
})
.then(function (pantry) {
t.strictSame(
pantry.ingredients['custom-only'].entryPoints.foo,
{
filename: 'index.foo'
},
'pantry.entryPoints[<predicate name>] is an object if match'
);
t.equal(
pantry.ingredients['custom-only'].entryPoints.bar,
null,
'pantry.entryPoints[<predicate name>] is null if no match'
);
})
.finally(restoreMockFS);
});
t.test('can be synchronous functions', function (t) {
mockfs({
'entrypoints-pantry': {
'custom-only': {
'ingredient.md': '',
'index.foo': ''
}
}
});
return initialize(
{
path: path.resolve('entrypoints-pantry'),
name: 'entrypoints-pantry',
predicates: {
foo: function (filename) {
return filename === 'index.foo';
},
bar: function (filename) {
return filename === 'index.bar';
},
}
})
.then(function (pantry) {
t.strictSame(
pantry.ingredients['custom-only'].entryPoints.foo,
{
filename: 'index.foo'
},
'pantry.entryPoints[<predicate name>] is an object if match'
);
t.equal(
pantry.ingredients['custom-only'].entryPoints.bar,
null,
'pantry.entryPoints[<predicate name>] is null if no match'
);
})
.finally(restoreMockFS);
});
t.test('can be asynchronous functions', function (t) {
mockfs({
'entrypoints-pantry': {
'custom-only': {
'ingredient.md': '',
'index.foo': ''
}
}
});
return initialize(
{
path: path.resolve('entrypoints-pantry'),
name: 'entrypoints-pantry',
predicates: {
foo: function (filename) {
return Promise.resolve(filename === 'index.foo');
},
bar: function (filename) {
return Promise.resolve(filename === 'index.bar');
},
}
})
.then(function (pantry) {
t.strictSame(
pantry.ingredients['custom-only'].entryPoints.foo,
{
filename: 'index.foo'
},
'pantry.entryPoints[<predicate name>] is an object if match'
);
t.equal(
pantry.ingredients['custom-only'].entryPoints.bar,
null,
'pantry.entryPoints[<predicate name>] is null if no match'
);
})
.finally(restoreMockFS);
});
t.test('can override default predicates', function (t) {
mockfs({
'entrypoints-pantry': {
'js-only': {
'ingredient.md': '',
'index.js': ''
},
'override-only': {
'ingredient.md': '',
'main.js': ''
}
}
});
return initialize(
{
path: path.resolve('entrypoints-pantry'),
name: 'entrypoints-pantry',
predicates: {
javaScript: /^main.js$/
}
})
.then(function (pantry) {
t.ok(
pantry.ingredients['override-only'].entryPoints.javaScript,
'pantry.entryPoints.javaScript is truthy if match'
);
t.notOk(
pantry.ingredients['js-only'].entryPoints.javaScript,
'pantry.entryPoints.javaScript is falsy if no match'
);
})
.finally(restoreMockFS);
});
});
});
t.test('accepts an optional node callback', function (t) {
t.autoend();
t.test('called with Pantry instance on success', function (t) {
mockfs({
'empty-pantry': {}
});
initialize(
{
path: path.resolve('empty-pantry'),
name: 'empty-pantry'
},
function (error, pantry) {
t.equals(error, null, 'error should be null');
t.type(pantry, Pantry);
t.end();
})
.finally(restoreMockFS);
});
t.test('called with error on failure', function (t) {
mockfs({
'empty-pantry': {}
});
initialize(
{
path: path.resolve('no-such-pantry'),
name: 'empty-pantry'
},
function (error, pantry) {
t.type(error, Error);
t.equals(pantry, undefined, 'pantry should be undefined');
})
.catch(function (e) {
if (!e.message.match(/Pantry .* does not exist\.$/)) {
throw e;
}
})
.finally(function () {
restoreMockFS();
t.end();
});
});
});
t.test('Pantry.ingredients', function (t) {
t.autoend();
t.test('is an object', function (t) {
mockfs({
'empty-pantry': {}
});
return initialize(
{
path: path.resolve('empty-pantry'),
name: 'empty-pantry'
})
.then(function (pantry) {
return t.type(pantry.ingredients, 'object');
})
.finally(restoreMockFS);
});
t.test('is an empty object if the pantry has no ingredients',
function (t) {
mockfs({
'empty-pantry': {}
});
return initialize(
{
path: path.resolve('empty-pantry'),
name: 'empty-pantry'
})
.then(function (pantry) {
return t.same(pantry.ingredients, {});
})
.finally(restoreMockFS);
});
t.test('has a key for each valid ingredient in the pantry', function (t) {
mockfs({
'simple-pantry': {
one: {
'ingredient.md': ''
},
two: {
'ingredient.md': ''
},
path: {
to: {
three: {
'ingredient.md': ''
}
}
}
}
});
return initialize(
{
path: path.resolve('simple-pantry'),
name: 'simple-pantry'
})
.then(function (pantry) {
return t.same(
_.keys(pantry.ingredients).sort(),
['one', 'two', 'path/to/three'].sort()
);
})
.finally(restoreMockFS);
});
t.test('ignores nested ingredients', function (t) {
mockfs({
'nested-pantry': {
'path': {
'ingredient.md': '',
'to': {
'three': {
'ingredient.md': ''
}
}
},
'prefix': {
'ingredient.md': ''
},
'prefixed': {
'ingredient.md': ''
},
'prefixedpath': {
'to': {
'another': {
'ingredient.md': ''
},
'anotherone': {
'ingredient.md': ''
}
}
}
}
});
return initialize(
{
path: path.resolve('nested-pantry'),
name: 'nested-pantry'
})
.then(function (pantry) {
return t.same(
_.keys(pantry.ingredients).sort(),
[
'prefix',
'prefixed',
'prefixedpath/to/another',
'prefixedpath/to/anotherone',
'path'
].sort()
);
})
.finally(restoreMockFS);
});
t.test('doesn\'t behave differently when package.json exists and roux.pantryRoot property is missing', function (t) {
mockfs({
'simple-pantry': {
'package.json': '{}',
one: {
'ingredient.md': ''
},
pantry: {
two: {
'ingredient.md': ''
},
path: {
to: {
three: {
'ingredient.md': ''
}
}
}
}
}
});
return initialize(
{
path: path.resolve('simple-pantry'),
name: 'simple-pantry'
})
.then(function (pantry) {
t.same(pantry.path, path.resolve('simple-pantry'), 'pantry path is unchanged');
t.same(
_.keys(pantry.ingredients).sort(),
['one', 'pantry/two', 'pantry/path/to/three'].sort(),
'ingredients are unchanged'
);
})
.finally(restoreMockFS);
});
t.test('doesn\'t behave differently when package.json exists and roux.pantryRoot property is set to a non-string value', function (t) {
mockfs({
'simple-pantry': {
'package.json': JSON.stringify({
roux: {
pantryRoot: 8675309
}
}),
one: {
'ingredient.md': ''
},
pantry: {
two: {
'ingredient.md': ''
},
path: {
to: {
three: {
'ingredient.md': ''
}
}
}
}
}
});
return initialize(
{
path: path.resolve('simple-pantry'),
name: 'simple-pantry'
})
.then(function (pantry) {
t.same(pantry.path, path.resolve('simple-pantry'), 'pantry path is unchanged');
t.same(
_.keys(pantry.ingredients).sort(),
['one', 'pantry/two', 'pantry/path/to/three'].sort(),
'ingredients are unchanged'
);
})
.finally(restoreMockFS);
});
t.test('respects custom pantry directory when package.json exists and contains a string value for roux.pantryRoot', function (t) {
mockfs({
'simple-pantry': {
'package.json': JSON.stringify({
roux: {
pantryRoot: 'pantry'
}
}),
one: {
'ingredient.md': ''
},
pantry: {
two: {
'ingredient.md': ''
},
path: {
to: {
three: {
'ingredient.md': ''
}
}
}
}
}
});
return initialize(
{
path: path.resolve('simple-pantry'),
name: 'simple-pantry'
})
.then(function (pantry) {
t.same(pantry.path, path.resolve('simple-pantry', 'pantry'), 'pantry path is updated');
t.same(
_.keys(pantry.ingredients).sort(),
['two', 'path/to/three'].sort(),
'ingredient paths are updated'
);
})
.finally(restoreMockFS);
});
t.test('respects custom pantry directory when package.json exists and contains a string value for roux.pantryRoot and pantry sub-directory doesn\'t exist', function (t) {
mockfs({
'simple-pantry': {
'package.json': JSON.stringify({
roux: {
pantryRoot: 'notPantry'
}
}),
one: {
'ingredient.md': ''
},
pantry: {
two: {
'ingredient.md': ''
},
path: {
to: {
three: {
'ingredient.md': ''
}
}
}
}
}
});
return initialize(
{
path: path.resolve('simple-pantry'),
name: 'simple-pantry'
})
.then(function (pantry) {
t.same(pantry.path, path.resolve('simple-pantry', 'notPantry'), 'pantry path is updated');
t.same(
_.keys(pantry.ingredients).sort(),
[],
'no ingredients are found in non-existent directory'
);
})
.finally(restoreMockFS);
});
t.test('each value is an Ingredient instance', function (t) {
mockfs({
'simple-pantry': {
one: {
'ingredient.md': ''
},
two: {
'ingredient.md': ''
},
path: {
to: {
three: {
'ingredient.md': ''
}
}
}
}
});
return initialize(
{
path: path.resolve('simple-pantry'),
name: 'simple-pantry'
})
.then(function (pantry) {
return t.ok(_.every(pantry.ingredients, function (ingredient) {
return ingredient instanceof Ingredient;
}), 'all ingredients should Ingredient instances');
})
.finally(restoreMockFS);
});
t.test('Pantry.ingredients[i].entryPoints', function (t) {
t.autoend();
t.test('is an object', function (t) {
mockfs({
'simple-pantry': {
one: {
'ingredient.md': ''
},
two: {
'ingredient.md': ''
},
path: {
to: {
three: {
'ingredient.md': ''
}
}
}
}
});
return initialize(
{
path: path.resolve('simple-pantry'),
name: 'simple-pantry'
})
.then(function (pantry) {
return t.ok(_.every(pantry.ingredients, function (ingredient) {
return typeof ingredient.entryPoints === 'object';
}), 'Pantry.ingredients[i].entryPoints should be an object');
})
.finally(restoreMockFS);
});
t.test('Pantry.ingredients[i].entryPoints.javaScript', function (t) {
mockfs({
'entrypoints-pantry': {
'js-only': {
'ingredient.md': '',
'index.js': ''
},
'sass-only': {
'ingredient.md': '',
'index.scss': ''
}
}
});
return initialize(
{
path: path.resolve('entrypoints-pantry'),
name: 'entrypoints-pantry'
})
.then(function (pantry) {
t.strictSame(
pantry.ingredients['js-only'].entryPoints.javaScript,
{
filename: 'index.js'
},
'is an object if ingredient has a JS entrypoint'
);
t.equal(
pantry.ingredients['sass-only'].entryPoints.javaScript,
null,
'null if ingredient does not have a JS entrypoint'
);
})
.finally(restoreMockFS);
});
t.test('Pantry.ingredients[i].entryPoints.sass', function (t) {
mockfs({
'entrypoints-pantry': {
'js-only': {
'ingredient.md': '',
'index.js': ''
},
'sass-only': {
'ingredient.md': '',
'index.scss': ''
}
}
});
return initialize(
{
path: path.resolve('entrypoints-pantry'),
name: 'entrypoints-pantry'
})
.then(function (pantry) {
t.strictSame(
pantry.ingredients['sass-only'].entryPoints.sass,
{
filename: 'index.scss'
},
'is an object if ingredient has a Sass entrypoint'
);
t.equal(
pantry.ingredients['js-only'].entryPoints.sass,
null,
'null if ingredient does not have a Sass entrypoint'
);
})
.finally(restoreMockFS);
});
});
});
});
t.type(
parseIngredientPath,
'function',
'exports a parseIngredientPath method'
);
t.test('parseIngredientPath', function (t) {
t.autoend();
t.test('ingredientPath', function (t) {
_.forEach(
[
0,
123,
true,
false,
null,
undefined,
[],
{}
],
function (arg) {
t.throws(function () {
parseIngredientPath({
pantries: arg
});
}, 'must be a string');
});
t.end();
});
t.equal(parseIngredientPath('foo'), null, 'null if <1 slash');
var expected = 'ingredient';
try {
sinon.spy(Ingredient, 'isValidName');
parseIngredientPath('pantry/ingredient');
t.ok(
Ingredient.isValidName.calledWithExactly(expected),
'the ingredient name is passed to `Ingredient.isValidName`'
);
Ingredient.isValidName.resetHistory();
parseIngredientPath('@namespace/pantry/ingredient');
t.ok(
Ingredient.isValidName.calledWithExactly(expected),
'the ingredient name is passed to `Ingredient.isValidName`'
);
} finally {
Ingredient.isValidName.restore();
}
try {
expected = 'pantry';
sinon.spy(Pantry, 'isValidName');
parseIngredientPath('pantry/ingredient');
t.ok(
Pantry.isValidName.calledWithExactly(expected),
'the pantry name is passed to `Pantry.isValidName`'
);
Pantry.isValidName.resetHistory();
expected = '@namespace/pantry';
parseIngredientPath('@namespace/pantry/ingredient');
t.ok(
Pantry.isValidName.calledWithExactly(expected),
'the pantry name is passed to `Pantry.isValidName`'
);
} finally {
Pantry.isValidName.restore();
}
try {
sinon.stub(Ingredient, 'isValidName');
sinon.stub(Pantry, 'isValidName');
Ingredient.isValidName.returns(false);
Pantry.isValidName.returns(true);
t.equal(
parseIngredientPath('pantry/ingredient'),
null,
'returns null if ingredient name is invalid'
);
Ingredient.isValidName.returns(true);
Pantry.isValidName.returns(false);
t.equal(
parseIngredientPath('pantry/ingredient'),
null,
'returns null if pantry name is invalid'
);
Ingredient.isValidName.returns(false);
Pantry.isValidName.returns(false);
t.equal(
parseIngredientPath('pantry/ingredient'),
null,
'returns null if both names are invalid'
);
Ingredient.isValidName.returns(true);
Pantry.isValidName.returns(true);
t.same(
parseIngredientPath('pantry/ingredient'),
{
pantry: 'pantry',
ingredient: 'ingredient'
},
'returns parsed name if both are valid'
);
} finally {
Ingredient.isValidName.restore();
Pantry.isValidName.restore();
}
t.same(
parseIngredientPath('@namespace/pantry/path/to/ingredient'),
{
pantry: '@namespace/pantry',
ingredient: 'path/to/ingredient'
},
'parses nested ingredients'
);
});
t.type(normalizeConfig, 'function', 'exports a normalizeConfig method');
t.test('normalizeConfig', function (t) {
t.autoend();
t.test('arguments', function (t) {
t.autoend();
testConfigArgValidation(t, 'config', normalizeConfig);
testConfigArgValidation(t, 'defaults', normalizeConfig.bind(null, undefined));
t.test('honors verious permutations of {config,defaults}', function (t) {
t.autoend();
t.test('new defaults for pantrySearchPaths', function (t) {
t.autoend();
var config = undefined;
var defaults = {
pantrySearchPaths: ['test/']
};
var normalized = normalizeConfig(config, defaults);
t.same(normalized.pantrySearchPaths, defaults.pantrySearchPaths);
});
t.test('config honored over new defaults for pantrySearchPaths', function (t) {
t.autoend();
var config = {
pantrySearchPaths: ['testing/']
};
var defaults = {
pantrySearchPaths: ['test/']
};
var normalized = normalizeConfig(config, defaults);
t.same(normalized.pantrySearchPaths, config.pantrySearchPaths);
});
t.test('new defaults for pantries', function (t) {
t.autoend();
var config = undefined;
var defaults = {
pantrySearchPaths: ['test/']
};
var normalized = normalizeConfig(config, defaults);
t.same(normalized.pantrySearchPaths, defaults.pantrySearchPaths);
});
t.test('config honored over new defaults for pantries', function (t) {
t.autoend();
var config = {
pantries: {testing: {}},
};
var defaults = {
pantries: {test: {}},
};
var normalized = normalizeConfig(config, defaults);
t.same(normalized.pantries, config.pantries);
});
});
});
});
t.type(resolve, 'function', 'exports a resolve method');
t.test('resolve', function (t) {
t.autoend();
t.test('arguments', function (t) {
t.autoend();
t.test('pantry', function (t) {
t.throws(function () {
resolve();
}, 'is required');
_.forEach(
[
0,
123,
true,
false,
null,
[],
{}
],
function (arg) {
t.throws(function () {
resolve(arg, 'ingredient', 'entryPoint', {});
}, 'must be a string');
});
_.forEach(
[
'',
'@foo',
'./foo',
'/foo',
'../foo/bar',
'foo:bar'
],
function (arg) {
t.throws(function () {
resolve(arg);
}, 'must be a valid npm package name ' + arg);
});
t.end();
});
t.test('ingredient', function (t) {
t.doesNotThrow(function () {
resolve('pantry').catch(_.noop);
}, 'is optional');
_.forEach(
[
0,
123,
true,
false,
null,
[],
{}
],
function (arg) {
t.throws(function () {
resolve('pantry', arg, 'entryPoint', {});
}, 'must be a string, not ' + arg);
});
t.end();
});
t.test('entryPoint', function (t) {
t.doesNotThrow(function () {
resolve('pantry', 'ingredient').catch(_.noop);
}, 'is optional');
_.forEach(
[
0,
123,
true,
false,
null,
[],
{}
],
function (arg) {
t.throws(function () {
resolve('pantry', 'ingredient', arg, {});
}, 'must be a string, not ' + arg);
});
t.end();
});
t.test('config', function (t) {
t.doesNotThrow(function () {
resolve('pantry', 'ingredient', 'entryPoint').catch(_.noop);
}, 'is optional');
_.forEach(
[
'',
'foo',
0,
123,
true,
false
],
function (arg) {
if (!_.isString(arg)) {
t.throws(function () {
resolve('pantry', arg);
}, 'must be an object, not ' + arg);
t.throws(function () {
resolve('pantry', 'ingredient', arg);
}, 'must be an object, not ' + arg);
}
t.throws(function () {
resolve('pantry', 'ingredient', 'entryPoint', arg);
}, 'must be an object, not ' + arg);
});
t.autoend();
t.test('config.pantries', function (t) {
t.doesNotThrow(function () {
resolve('pantry', {}).catch(_.noop);
resolve('pantry', 'ingredient', {}).catch(_.noop);
resolve('pantry', 'ingredient', 'entryPoint', {}).catch(_.noop);
}, 'is optional');
_.forEach(
[
'',
'foo',
0,
123,
true,
false
],
function (arg) {
t.throws(function () {
resolve('pantry', {pantries: arg});
}, 'must be an object, not ' + arg);
t.throws(function () {
resolve('pantry', 'ingredient', {pantries: arg});
}, 'must be an object, not ' + arg);
t.throws(function () {
resolve(
'pantry', 'ingredient', 'entryPoint', {pantries: arg});
}, 'must be an object, not ' + arg);
});
t.end();
});
t.test('config.pantrySearchPaths', function (t) {
t.doesNotThrow(function () {
resolve('pantry', {}).catch(_.noop);
resolve('pantry', 'ingredient', {}).catch(_.noop);
resolve('pantry', 'ingredient', 'entryPoint', {}).catch(_.noop);
}, 'is optional');
_.forEach(
[
'',
'foo',
0,
123,
true,
false,
{}
],
function (arg) {
t.throws(function () {
resolve('pantry', {pantrySearchPaths: arg});
}, 'must be an array, not ' + arg);
t.throws(function () {
resolve('pantry', 'ingredient', {pantrySearchPaths: arg});
}, 'must be an array, not ' + arg);
t.throws(function () {
resolve(
'pantry',
'ingredient',
'entryPoint',
{
pantrySearchPaths: arg
}
);
}, 'must be an array, not ' + arg);
});
t.autoend();
t.test('defaults to `["$CWD/node_modules"]`', function (t) {
mockfs({
node_modules: {
pantry: {
ingredient: {
'ingredient.md': ''
}
}
}
});
var expectedPath = path.resolve('node_modules', 'pantry');
return resolve('pantry', {})
.then(function (actual) {
t.type(actual, Pantry, 'looks up pantry in node_modules');
t.equal(actual.path, expectedPath);
})
.finally(restoreMockFS);
});
});
});
});
t.test('returns a promise', function (t) {
_.forEach(
[
resolve('pantry'),
resolve('pantry', {}),
resolve('pantry', 'ingredient'),
resolve('pantry', 'ingredient', {}),
resolve('pantry', 'ingredient', 'entry point'),
resolve('pantry', 'ingredient', 'entry point', {})
],
function (returnValue) {
t.type(returnValue.then, 'function', 'return value is a promise');
returnValue.catch(_.noop);
});
t.end();
});
t.test('if passed a pantry only', function (t) {
t.autoend();
t.test('returns pantry from `config.pantries` if present', function (t) {
var expected = new Pantry({
ingredients: {}
});
return resolve('pantry', {
pantries: {
pantry: expected
}
})
.then(function (actual) {
t.same(actual, expected, 'resolves to cached pantry');
});
});
t.test('looks up pantry in search paths if not cached', function (t) {
t.autoend();
t.test('resolves if found', function (t) {
mockfs({
resolve: {
pantry: {
ingredient: {
'ingredient.md': ''
}
}
}
});
return resolve('pantry', {
pantrySearchPaths: [path.resolve('resolve')]
})
.then(function (actual) {
t.type(actual, Pantry, 'looks up pantry in search paths');
})
.finally(restoreMockFS);
});
t.test('rejects if not found', function (t) {
mockfs({
resolve: {}
});
return resolve('no-such-pantry', {
pantrySearchPaths: [path.resolve('resolve')]
})
.catch(function (error) {
t.type(error, Error, 'resolves with an error if not found');
})
.finally(restoreMockFS);
});
});
});
t.test('if passed a string', function (t) {
t.autoend();
t.test('initializes to the specified location on the file system', function (t) {
t.autoend();
t.test('resolves if found', function (t) {
mockfs({
resolve: {
pantry: {
ingredient: {
'ingredient.md': ''
}
}
}
});
return resolve('pantry', {
pantries: {
pantry: path.resolve('resolve/pantry')
}
})
.then(function (actual) {
t.type(actual, Pantry, 'resolves string to pantry');
})
.finally(restoreMockFS);
});
t.test('modifies the pantry', function (t) {
mockfs({
resolve: {
pantry: {
ingredient: {
'ingredient.md': ''
}
}
}
});
var config = {
pantries: {
pantry: path.resolve('resolve/pantry')
}
};
return resolve('pantry', config)
.then(function (actual) {
t.type(config.pantries.pantry, Pantry, 'modifies the cache');
t.same(config.pantries.pantry, actual, 'modifies the cache');
})
.finally(restoreMockFS);
});
});
});
t.test('if passed a pantry and ingredient only', function (t) {
t.autoend();
t.test('looks up ingredient in `config.pantries`',
function (t) {
var expected = new Pantry({
ingredients: {
ingredient: {}
}
});
return resolve('pantry', 'ingredient', {
pantries: {
pantry: expected
}
})
.then(function (actual) {
t.same(
actual,
expected.ingredients.ingredient,
'resolves to ingredient from cached pantry'
);
});
});
t.test('looks up ingredient in `config.pantrySearchPaths`', function (t) {
mockfs({
resolve: {
pantry: {
path: {
to: {
ingredient: {
'ingredient.md': ''
}
}
}
}
}
});
return resolve('pantry', 'path/to/ingredient', {
pantrySearchPaths: [path.resolve('resolve')]
})
.then(function (actual) {
t.type(actual, Ingredient, 'looks up ingredient in search paths');
})
.finally(restoreMockFS);
});
t.test('rejects if not found', function (t) {
mockfs({
resolve: {
pantry: {
ingredient: {
'ingredient.md': ''
}
}
}
});
return resolve('pantry', 'no-such-ingredient', {
pantrySearchPaths: [path.resolve('resolve')]
})
.catch(function (error) {
t.type(error, Error, 'resolves with an error if not found');
})
.finally(restoreMockFS);
});
t.test('rejects with PantryDoesNotExistError if the pantry is not found', function (t) {
mockfs({
resolve: {
pantry: {
ingredient: {
'ingredient.md': ''
}
},
pantryTwo: {
ingredient: {
'ingredient.md': ''
}
}
}
});
return resolve('no-such-pantry', 'ingredient', {
pantrySearchPaths: [path.resolve('resolve')]
})
.catch(function (error) {
t.type(error, pantryErrors.PantryDoesNotExistError, 'rejects with a PantryDoesNotExistError if pantry not found');
})
.finally(restoreMockFS);
});
t.test('rejects with PantryNotADirectoryError if the pantry is found but not a directory', function (t) {
mockfs({
resolve: {
pantry: 'this is a file'
}
});
return resolve('pantry', 'ingredient', {
pantrySearchPaths: [path.resolve('resolve')]
})
.catch(function (error) {
t.type(error, pantryErrors.PantryNotADirectoryError, 'rejects with a PantryNotADirectoryError if pantry found but not a directory');
})
.finally(restoreMockFS);
});
t.test('rejects with IngredientDoesNotExistError if the ingredient is not found', function (t) {
mockfs({
resolve: {
pantry: {
ingredient: {
'ingredient.md': ''
}
}
}
});
return resolve('pantry', 'no-such-ingredient', {
pantrySearchPaths: [path.resolve('resolve')]
})
.catch(function (error) {
t.type(error, pantryErrors.IngredientDoesNotExistError, 'rejects with a IngredientDoesNotExistError if ingredient not found');
})
.finally(restoreMockFS);
});
t.test('rejects with IngredientHasNoSuchEntrypointError if' +
'ingredient entryPoint not found',
function (t) {
mockfs({
resolve: {
pantry: {
ingredient: {
'ingredient.md': ''
}
}
}
});
return resolve('pantry', 'ingredient', 'handlebars', {
pantrySearchPaths: [path.resolve('resolve')]
})
.catch(function (error) {
t.type(
error,
pantryErrors.IngredientHasNoSuchEntrypointError,
'rejects with a IngredientHasNoSuchEntrypointError if' +
'the ingredient has no such entrypoint'
);
})
.finally(restoreMockFS);
}
);
});
t.test('if passed a pantry, ingredient, and entry point', function (t) {
t.autoend();
t.test('returns entry point path from `config.pantries` if present',
function (t) {
var expected = '/path/to/pantry/and/ingredient/index.foo';
return resolve('pantry', 'ingredient', 'entryPoint', {
pantries: {
pantry: new Pantry({
ingredients: {
ingredient: {
path: '/path/to/pantry/and/ingredient',
entryPoints: {
entryPoint: {
filename: 'index.foo'
}
}
}
}
})
}
})
.then(function (actual) {
t.same(
actual,
expected,
'resolves to the entry point from the cached pantry'
);
});
});
t.test('looks up entry point in `config.pantrySearchPaths`',
function (t) {
mockfs({
resolve: {
pantry: {
ingredient: {
'ingredient.md': '',
'index.js': ''
}
}
}
});
var expected =
path.resolve('resolve', 'pantry', 'ingredient', 'index.js');
return resolve('pantry', 'ingredient', 'javaScript', {
pantrySearchPaths: [path.resolve('resolve')]
})
.then(function (actual) {
t.equal(actual, expected, 'looks up entry point in search paths');
})
.finally(restoreMockFS);
});
t.test('rejects if not found', function (t) {
mockfs({
resolve: {
pantry: {
ingredient: {
'ingredient.md': ''
}
}
}
});
return resolve('pantry', 'ingredient', 'no-such-entry-point', {
pantrySearchPaths: [path.resolve('resolve')]
})
.catch(function (error) {
t.type(error, Error, 'resolves with an error if not found');
})
.finally(restoreMockFS);
});
});
});
});
tap.test('Ingredient', function (t) {
t.autoend();
t.type(
Ingredient.isValidName,
'function',
'provides a static `isValidName` method'
);
t.test('Ingredient.isValidName', function (t) {
t.autoend();
var Ingredient = require('../lib/ingredient');
t.ok(Ingredient.isValidName('ingredient'));
t.ok(Ingredient.isValidName('ingredient2'));
t.ok(Ingredient.isValidName('ingredient_Two'));
t.ok(Ingredient.isValidName('ingredient-two'));
t.ok(Ingredient.isValidName('ingredient-two_3Four'));
t.ok(Ingredient.isValidName('ingredient/one-1/Two_2/3'));
t.notOk(Ingredient.isValidName('@ingredient'));
t.notOk(Ingredient.isValidName('an ingredient'));
});
});
tap.test('Pantry', function (t) {
t.autoend();
t.type(
Pantry.isValidName,
'function',
'provides a static `isValidName` method'
);
t.test('Pantry.isValidName', function (t) {
mockery.enable({
useCleanCache: true,
warnOnUnregistered: false
});
mockery.registerMock('validate-npm-package-name', sinon.stub().returns({
validForOldPackages: false,
validForNewPackages: true
}));
var mockValidateNpmPackageName = require('validate-npm-package-name');
var Pantry = require('../lib/pantry');
var expected = 'ingredient-name';
try {
Pantry.isValidName(expected);
t.ok(
mockValidateNpmPackageName.calledWithExactly(expected),
'delegates to `validate-npm-package-name`'
);
mockValidateNpmPackageName.returns({
validForOldPackages: false,
validForNewPackages: true
});
t.equal(
Pantry.isValidName(expected),
true,
'returns the `validForNewPackages` property value from the result of ' +
'calling `validate-npm-package-name`'
);
mockValidateNpmPackageName.returns({
validForOldPackages: true,
validForNewPackages: false
});
t.equal(
Pantry.isValidName(expected),
false,
'returns the `validForNewPackages` property value from the result of ' +
'calling `validate-npm-package-name`'
);
} finally {
mockery.deregisterMock('validate-npm-package-name');
mockery.disable();
t.end();
}
});
t.type(
Pantry.isPantry,
'function',
'provides a static `isPantry` method'
);
t.test('Pantry.isPantry', function (t) {
t.notOk(Pantry.isPantry('foo'));
t.notOk(Pantry.isPantry(1));
t.notOk(Pantry.isPantry(null));
t.notOk(Pantry.isPantry({}));
t.notOk(Pantry.isPantry(function () {}));
t.ok(Pantry.isPantry(new Pantry({})));
t.end();
});
});
function testConfigArgValidation(t, argName, methodToTest) {
t.test(argName, function (t) {
t.autoend();
t.test('does not mutate the original argument', function (t) {
t.autoend();
var obj = {};
methodToTest(obj);
t.same(obj, {});
});
_.forEach(
[
'',
'foo',
0,
123,
true,
false
],
function (arg) {
t.throws(function () {
methodToTest(arg);
}, 'must be an object, not ' + arg);
});
t.test(argName + '.pantries', function (t) {
t.doesNotThrow(function () {
methodToTest({});
}, 'is optional');
_.forEach(
[
'',
'foo',
0,
123,
true,
false
],
function (arg) {
t.throws(function () {
methodToTest({pantries: arg});
}, 'must be an object, not ' + arg);
});
t.end();
});
t.test(argName + '.pantrySearchPaths', function (t) {
t.autoend();
t.doesNotThrow(function () {
methodToTest({});
}, 'is optional');
_.forEach(
[
'',
'foo',
0,
123,
true,
false,
{}
],
function (arg) {
t.throws(function () {
methodToTest({pantrySearchPaths: arg});
}, 'must be an array, not ' + arg);
});
t.test('defaults to `["$CWD/node_modules"]`', function (t) {
t.autoend();
var expectedPath = path.resolve('node_modules');
var normalized = methodToTest({});
t.equal(normalized.pantrySearchPaths[0], expectedPath);
});
});
});
}