ember-cli
Version:
Command line tool for developing ambitious ember.js apps
777 lines (623 loc) • 27.7 kB
JavaScript
;
var fs = require('fs');
var Blueprint = require('../../../lib/models/blueprint');
var Task = require('../../../lib/models/task');
var MockProject = require('../../helpers/mock-project');
var MockUI = require('../../helpers/mock-ui');
var assert = require('assert');
var path = require('path');
var walkSync = require('walk-sync');
var Promise = require('../../../lib/ext/promise');
var rimraf = Promise.denodeify(require('rimraf'));
var EOL = require('os').EOL;
var root = process.cwd();
var tmp = require('tmp-sync');
var tmproot = path.join(root, 'tmp');
var SilentError = require('../../../lib/errors/silent');
var defaultBlueprints = path.resolve(__dirname, '..', '..', '..', 'blueprints');
var fixtureBlueprints = path.resolve(__dirname, '..', '..', 'fixtures', 'blueprints');
var basicBlueprint = path.join(fixtureBlueprints, 'basic');
var basicNewBlueprint = path.join(fixtureBlueprints, 'basic_2');
var defaultIgnoredFiles = Blueprint.ignoredFiles;
var basicBlueprintFiles = [
'.ember-cli',
'.gitignore',
'foo.txt',
'test.txt'
];
assert.match = function(actual, matcher) {
assert(matcher.test(actual), 'expected: ' +
actual +
' to match ' +
matcher);
};
describe('Blueprint', function() {
beforeEach(function() {
Blueprint.ignoredFiles = defaultIgnoredFiles;
});
describe('.mapFile', function() {
it('replaces all occurences of __name__ with module name',function(){
var path = Blueprint.prototype.mapFile('__name__/__name__-controller.js',{dasherizedModuleName: 'my-blueprint'});
assert.equal(path,'my-blueprint/my-blueprint-controller.js');
path = Blueprint.prototype.mapFile('__name__/controller.js',{dasherizedModuleName: 'my-blueprint'});
assert.equal(path,'my-blueprint/controller.js');
path = Blueprint.prototype.mapFile('__name__/__name__.js',{dasherizedModuleName: 'my-blueprint'});
assert.equal(path,'my-blueprint/my-blueprint.js');
});
it('accepts locals.fileMap with multiple mappings',function(){
var locals = {};
locals.fileMap= {
__name__: 'user',
__type__: 'controller',
__path__: 'pods/users',
__plural__: ''
};
var path = Blueprint.prototype.mapFile('__name__/__type____plural__.js',locals);
assert.equal(path,'user/controller.js');
path = Blueprint.prototype.mapFile('__path__/__name__/__type__.js',locals);
assert.equal(path,'pods/users/user/controller.js');
});
});
describe('.fileMapTokens', function() {
it('adds additional tokens from fileMapTokens hook', function() {
var blueprint = Blueprint.lookup(basicBlueprint);
blueprint.fileMapTokens = function() {
return {
__foo__: function(){
return 'foo';
}
};
};
var tokens = blueprint._fileMapTokens();
assert.equal(tokens.__foo__(), 'foo');
});
});
describe('.lookup', function() {
it('uses an explicit path if one is given', function() {
var expectedClass = require(basicBlueprint);
var blueprint = Blueprint.lookup(basicBlueprint);
assert.equal(blueprint.name, 'basic');
assert.equal(blueprint.path, basicBlueprint);
assert(blueprint instanceof expectedClass);
});
it('finds blueprints within given lookup paths', function() {
var expectedClass = require(basicBlueprint);
var blueprint = Blueprint.lookup('basic', {
paths: [fixtureBlueprints]
});
assert.equal(blueprint.name, 'basic');
assert.equal(blueprint.path, basicBlueprint);
assert(blueprint instanceof expectedClass);
});
it('finds blueprints in the ember-cli package', function() {
var expectedPath = path.resolve(defaultBlueprints, 'app');
var expectedClass = Blueprint;
var blueprint = Blueprint.lookup('app');
assert.equal(blueprint.name, 'app');
assert.equal(blueprint.path, expectedPath);
assert(blueprint instanceof expectedClass);
});
it('can instantiate a blueprint that exports an object instead of a constructor', function() {
var blueprint = Blueprint.lookup('exporting-object', {
paths: [fixtureBlueprints]
});
assert.equal(blueprint.woot, 'someValueHere');
assert(blueprint instanceof Blueprint);
});
it('throws an error if no blueprint is found', function() {
assert.throws(function() {
Blueprint.lookup('foo');
}, 'Unknown blueprint: foo');
});
it('returns undefined if no blueprint is found and ignoredMissing is passed', function() {
var blueprint = Blueprint.lookup('foo', {
ignoreMissing: true
});
assert.equal(blueprint, undefined);
});
});
describe('.list', function() {
it('returns a list of blueprints grouped by lookup path', function() {
var list = Blueprint.list({ paths: [fixtureBlueprints] });
var actual = list[0];
var expected = {
source: 'fixtures',
blueprints: [{
name: 'basic',
description: 'A basic blueprint',
overridden: false
}, {
name: 'basic_2',
description: 'Another basic blueprint',
overridden: false
}, {
name: 'exporting-object',
description: 'A blueprint that exports an object',
overridden: false
}, {
name: 'with-templating',
description: 'A blueprint with templating',
overridden: false
}]
};
assert.deepEqual(actual[0], expected[0]);
});
});
it('exists', function() {
var blueprint = new Blueprint(basicBlueprint);
assert(blueprint);
});
it('derives name from path', function() {
var blueprint = new Blueprint(basicBlueprint);
assert.equal(blueprint.name, 'basic');
});
describe('basic blueprint installation', function() {
var blueprint;
var ui;
var project;
var options;
var tmpdir;
beforeEach(function() {
tmpdir = tmp.in(tmproot);
blueprint = new Blueprint(basicBlueprint);
ui = new MockUI();
project = new MockProject();
options = {
ui: ui,
project: project,
target: tmpdir
};
});
afterEach(function() {
return rimraf(tmproot);
});
it('installs basic files', function() {
assert(blueprint);
return blueprint.install(options)
.then(function() {
var actualFiles = walkSync(tmpdir).sort();
var output = ui.output.trim().split(EOL);
assert.match(output.shift(), /^installing/);
assert.match(output.shift(), /create.* .ember-cli/);
assert.match(output.shift(), /create.* .gitignore/);
assert.match(output.shift(), /create.* foo.txt/);
assert.match(output.shift(), /create.* test.txt/);
assert.equal(output.length, 0);
assert.deepEqual(actualFiles, basicBlueprintFiles);
});
});
it('re-installing identical files', function() {
return blueprint.install(options)
.then(function() {
var output = ui.output.trim().split(EOL);
ui.output = '';
assert.match(output.shift(), /^installing/);
assert.match(output.shift(), /create.* \.ember-cli/);
assert.match(output.shift(), /create.* \.gitignore/);
assert.match(output.shift(), /create.* foo.txt/);
assert.match(output.shift(), /create.* test.txt/);
assert.equal(output.length, 0);
return blueprint.install(options);
})
.then(function() {
var actualFiles = walkSync(tmpdir).sort();
var output = ui.output.trim().split(EOL);
assert.match(output.shift(), /^installing/);
assert.match(output.shift(), /identical.* \.ember-cli/);
assert.match(output.shift(), /identical.* \.gitignore/);
assert.match(output.shift(), /identical.* foo.txt/);
assert.match(output.shift(), /identical.* test.txt/);
assert.equal(output.length, 0);
assert.deepEqual(actualFiles, basicBlueprintFiles);
});
});
it('re-installing conflicting files', function() {
return blueprint.install(options)
.then(function() {
var output = ui.output.trim().split(EOL);
ui.output = '';
assert.match(output.shift(), /^installing/);
assert.match(output.shift(), /create.* \.ember-cli/);
assert.match(output.shift(), /create.* \.gitignore/);
assert.match(output.shift(), /create.* foo.txt/);
assert.match(output.shift(), /create.* test.txt/);
assert.equal(output.length, 0);
var blueprintNew = new Blueprint(basicNewBlueprint);
setTimeout(function(){
ui.inputStream.write('n' + EOL);
}, 25);
setTimeout(function(){
ui.inputStream.write('y' + EOL);
}, 50);
return blueprintNew.install(options);
})
.then(function() {
var actualFiles = walkSync(tmpdir).sort();
// Prompts contain \n EOL
// Split output on \n since it will have the same affect as spliting on OS specific EOL
var output = ui.output.trim().split('\n');
assert.match(output.shift(), /^installing/);
assert.match(output.shift(), /Overwrite.*foo.*\?/); // Prompt
assert.match(output.shift(), /Overwrite.*foo.*No, skip/);
assert.match(output.shift(), /Overwrite.*test.*\?/); // Prompt
assert.match(output.shift(), /Overwrite.*test.*Yes, overwrite/);
assert.match(output.shift(), /identical.* \.ember-cli/);
assert.match(output.shift(), /identical.* \.gitignore/);
assert.match(output.shift(), /skip.* foo.txt/);
assert.match(output.shift(), /overwrite.* test.txt/);
assert.equal(output.length, 0);
assert.deepEqual(actualFiles, basicBlueprintFiles);
});
});
describe('called on an existing project', function() {
beforeEach(function() {
Blueprint.ignoredUpdateFiles.push('foo.txt');
});
it('ignores files in ignoredUpdateFiles', function() {
return blueprint.install(options)
.then(function() {
var output = ui.output.trim().split(EOL);
ui.output = '';
assert.match(output.shift(), /^installing/);
assert.match(output.shift(), /create.* \.ember-cli/);
assert.match(output.shift(), /create.* \.gitignore/);
assert.match(output.shift(), /create.* foo.txt/);
assert.match(output.shift(), /create.* test.txt/);
assert.equal(output.length, 0);
var blueprintNew = new Blueprint(basicNewBlueprint);
setTimeout(function(){
ui.inputStream.write('n' + EOL);
}, 25);
setTimeout(function(){
ui.inputStream.write('n'+ EOL);
}, 50);
options.project.isEmberCLIProject = function() { return true; };
return blueprintNew.install(options);
})
.then(function() {
var actualFiles = walkSync(tmpdir).sort();
// Prompts contain \n EOL
// Split output on \n since it will have the same affect as spliting on OS specific EOL
var output = ui.output.trim().split('\n');
assert.match(output.shift(), /^installing/);
assert.match(output.shift(), /Overwrite.*test.*\?/); // Prompt
assert.match(output.shift(), /Overwrite.*test.*No, skip/);
assert.match(output.shift(), /identical.* \.ember-cli/);
assert.match(output.shift(), /identical.* \.gitignore/);
assert.match(output.shift(), /skip.* test.txt/);
assert.equal(output.length, 0);
assert.deepEqual(actualFiles, basicBlueprintFiles);
});
});
});
it('throws error when there is a trailing forward slash in entityName', function(){
options.entity = { name: 'foo/' };
assert.throws(function(){
blueprint.install(options);
}, /You specified "foo\/", but you can't use a trailing slash as an entity name with generators. Please re-run the command with "foo"./);
options.entity = { name: 'foo\\' };
assert.throws(function(){
blueprint.install(options);
}, /You specified "foo\\", but you can't use a trailing slash as an entity name with generators. Please re-run the command with "foo"./);
options.entity = { name: 'foo' };
assert.doesNotThrow(function(){
blueprint.install(options);
});
});
it('throws error when an entityName is not provided', function(){
options.entity = { };
assert.throws(function(){
blueprint.install(options);
}, SilentError, /'The `ember generate` command requires an entity name to be specified./);
});
it('calls normalizeEntityName hook during install', function(done){
blueprint.normalizeEntityName = function(){ done(); };
options.entity = { name: 'foo' };
blueprint.install(options);
});
it('normalizeEntityName hook can modify the entity name', function(){
blueprint.normalizeEntityName = function(){ return 'foo'; };
options.entity = { name: 'bar' };
return blueprint.install(options)
.then(function() {
var actualFiles = walkSync(tmpdir).sort();
assert.deepEqual(actualFiles, basicBlueprintFiles);
});
});
it('calls normalizeEntityName before locals hook is called', function(done) {
blueprint.normalizeEntityName = function(){ return 'foo'; };
blueprint.locals = function(options) {
assert.equal(options.entity.name, 'foo');
done();
};
options.entity = { name: 'bar' };
blueprint.install(options);
});
});
describe('addPackageToProject', function() {
var blueprint;
var ui;
var tmpdir;
beforeEach(function() {
tmpdir = tmp.in(tmproot);
blueprint = new Blueprint(basicBlueprint);
ui = new MockUI();
});
afterEach(function() {
return rimraf(tmproot);
});
it('calls _exec with the proper command when no version is supplied', function() {
blueprint._exec = function(command) {
assert.equal(command, 'npm install --save-dev foo-bar');
};
blueprint.addPackageToProject('foo-bar');
});
it('calls _exec with the proper command when a version is supplied', function() {
blueprint._exec = function(command) {
assert.equal(command, 'npm install --save-dev foo-bar@^123.1.12');
};
blueprint.addPackageToProject('foo-bar', '^123.1.12');
});
it('writes information to the ui log', function() {
blueprint._exec = function() { };
blueprint.ui = ui;
blueprint.addPackageToProject('foo-bar', '^123.1.12');
var output = ui.output.trim();
assert.match(output, /install package.*foo-bar/);
});
it('does not error if ui is not present', function() {
blueprint._exec = function() { };
delete blueprint.ui;
blueprint.addPackageToProject('foo-bar', '^123.1.12');
var output = ui.output.trim();
assert(!output.match(/install package.*foo-bar/));
});
});
describe('addBowerPackageToProject', function() {
var blueprint;
var ui;
var tmpdir;
var BowerInstallTask;
var taskNameLookedUp;
beforeEach(function() {
tmpdir = tmp.in(tmproot);
blueprint = new Blueprint(basicBlueprint);
ui = new MockUI();
blueprint.taskFor = function(name) {
taskNameLookedUp = name;
return new BowerInstallTask();
};
});
afterEach(function() {
return rimraf(tmproot);
});
it('looks up the `bower-install` task', function() {
BowerInstallTask = Task.extend({
run: function() {}
});
blueprint.addBowerPackageToProject('foo-bar');
assert.equal(taskNameLookedUp, 'bower-install');
});
it('calls the task with the package name', function() {
var packages;
BowerInstallTask = Task.extend({
run: function(options) {
packages = options.packages;
}
});
blueprint.addBowerPackageToProject('foo-bar');
assert.deepEqual(packages, ['foo-bar']);
});
it('uses the provided target (version, range, sha, etc)', function() {
var packages;
BowerInstallTask = Task.extend({
run: function(options) {
packages = options.packages;
}
});
blueprint.addBowerPackageToProject('foo-bar', '~1.0.0');
assert.deepEqual(packages, ['foo-bar#~1.0.0']);
});
it('uses uses verbose mode with the task', function() {
var verbose;
BowerInstallTask = Task.extend({
run: function(options) {
verbose = options.verbose;
}
});
blueprint.addBowerPackageToProject('foo-bar', '~1.0.0');
assert(verbose);
});
});
describe('insertIntoFile', function() {
var blueprint;
var ui;
var tmpdir;
var project;
var filename;
beforeEach(function() {
tmpdir = tmp.in(tmproot);
blueprint = new Blueprint(basicBlueprint);
ui = new MockUI();
project = new MockProject();
// normally provided by `install`, but mocked here for testing
project.root = tmpdir;
blueprint.project = project;
filename = 'foo-bar-baz.txt';
});
afterEach(function() {
return rimraf(tmproot);
});
it('will create the file if not already existing', function() {
var toInsert = 'blahzorz blammo';
return blueprint.insertIntoFile(filename, toInsert)
.then(function(result) {
var contents = fs.readFileSync(path.join(project.root, filename), { encoding: 'utf8' });
assert(contents.indexOf(toInsert) > -1, 'contents were inserted');
assert.equal(result.originalContents, '', 'returned object should contain original contents');
assert(result.inserted, 'inserted should indicate that the file was modified');
assert.equal(contents, result.contents, 'returned object should contain contents');
});
});
it('will insert into the file if it already exists', function() {
var toInsert = 'blahzorz blammo';
var originalContent = 'some original content\n';
var filePath = path.join(project.root, filename);
fs.writeFileSync(filePath, originalContent, { encoding: 'utf8' });
return blueprint.insertIntoFile(filename, toInsert)
.then(function(result) {
var contents = fs.readFileSync(path.join(project.root, filename), { encoding: 'utf8' });
assert.equal(contents, originalContent + toInsert, 'inserted contents should be appended to original');
assert.equal(result.originalContents, originalContent, 'returned object should contain original contents');
assert(result.inserted, 'inserted should indicate that the file was modified');
});
});
it('will not insert into the file if it already contains the content', function() {
var toInsert = 'blahzorz blammo';
var filePath = path.join(project.root, filename);
fs.writeFileSync(filePath, toInsert, { encoding: 'utf8' });
return blueprint.insertIntoFile(filename, toInsert)
.then(function(result) {
var contents = fs.readFileSync(path.join(project.root, filename), { encoding: 'utf8' });
assert.equal(contents, toInsert, 'contents should be unchanged');
assert(!result.inserted, 'inserted should indicate that the file was not modified');
});
});
it('will insert into the file if it already contains the content if force option is passed', function() {
var toInsert = 'blahzorz blammo';
var filePath = path.join(project.root, filename);
fs.writeFileSync(filePath, toInsert, { encoding: 'utf8' });
return blueprint.insertIntoFile(filename, toInsert, { force: true })
.then(function(result) {
var contents = fs.readFileSync(path.join(project.root, filename), { encoding: 'utf8' });
assert.equal(contents, toInsert + toInsert, 'contents should be unchanged');
assert(result.inserted, 'inserted should indicate that the file was not modified');
});
});
it('will insert into the file after a specified string if options.after is specified', function(){
var toInsert = 'blahzorz blammo';
var line1 = 'line1 is here';
var line2 = 'line2 here';
var line3 = 'line3';
var originalContent = [line1, line2, line3].join(EOL);
var filePath = path.join(project.root, filename);
fs.writeFileSync(filePath, originalContent, { encoding: 'utf8' });
return blueprint.insertIntoFile(filename, toInsert, {after: line2 + EOL})
.then(function(result) {
var contents = fs.readFileSync(path.join(project.root, filename), { encoding: 'utf8' });
assert.equal(contents, [line1, line2, toInsert, line3].join(EOL),
'inserted contents should be inserted after the `after` value');
assert.equal(result.originalContents, originalContent, 'returned object should contain original contents');
assert(result.inserted, 'inserted should indicate that the file was modified');
});
});
it('will insert into the file after the first instance of options.after only', function(){
var toInsert = 'blahzorz blammo';
var line1 = 'line1 is here';
var line2 = 'line2 here';
var line3 = 'line3';
var originalContent = [line1, line2, line2, line3].join(EOL);
var filePath = path.join(project.root, filename);
fs.writeFileSync(filePath, originalContent, { encoding: 'utf8' });
return blueprint.insertIntoFile(filename, toInsert, {after: line2 + EOL})
.then(function(result) {
var contents = fs.readFileSync(path.join(project.root, filename), { encoding: 'utf8' });
assert.equal(contents, [line1, line2, toInsert, line2, line3].join(EOL),
'inserted contents should be inserted after the `after` value');
assert.equal(result.originalContents, originalContent, 'returned object should contain original contents');
assert(result.inserted, 'inserted should indicate that the file was modified');
});
});
it('will insert into the file before a specified string if options.before is specified', function(){
var toInsert = 'blahzorz blammo';
var line1 = 'line1 is here';
var line2 = 'line2 here';
var line3 = 'line3';
var originalContent = [line1, line2, line3].join(EOL);
var filePath = path.join(project.root, filename);
fs.writeFileSync(filePath, originalContent, { encoding: 'utf8' });
return blueprint.insertIntoFile(filename, toInsert, {before: line2 + EOL})
.then(function(result) {
var contents = fs.readFileSync(path.join(project.root, filename), { encoding: 'utf8' });
assert.equal(contents, [line1, toInsert, line2, line3].join(EOL),
'inserted contents should be inserted before the `before` value');
assert.equal(result.originalContents, originalContent, 'returned object should contain original contents');
assert(result.inserted, 'inserted should indicate that the file was modified');
});
});
it('will insert into the file before the first instance of options.before only', function(){
var toInsert = 'blahzorz blammo';
var line1 = 'line1 is here';
var line2 = 'line2 here';
var line3 = 'line3';
var originalContent = [line1, line2, line2, line3].join(EOL);
var filePath = path.join(project.root, filename);
fs.writeFileSync(filePath, originalContent, { encoding: 'utf8' });
return blueprint.insertIntoFile(filename, toInsert, {before: line2 + EOL})
.then(function(result) {
var contents = fs.readFileSync(path.join(project.root, filename), { encoding: 'utf8' });
assert.equal(contents, [line1, toInsert, line2, line2, line3].join(EOL),
'inserted contents should be inserted after the `after` value');
assert.equal(result.originalContents, originalContent, 'returned object should contain original contents');
assert(result.inserted, 'inserted should indicate that the file was modified');
});
});
it('it will make no change if options.after is not found in the original', function(){
var toInsert = 'blahzorz blammo';
var originalContent = 'the original content';
var filePath = path.join(project.root, filename);
fs.writeFileSync(filePath, originalContent, { encoding: 'utf8' });
return blueprint.insertIntoFile(filename, toInsert, {after: 'not found' + EOL})
.then(function(result) {
var contents = fs.readFileSync(path.join(project.root, filename), { encoding: 'utf8' });
assert.equal(contents, originalContent, 'original content is unchanged');
assert.equal(result.originalContents, originalContent, 'returned object should contain original contents');
assert(!result.inserted, 'inserted should indicate that the file was not modified');
});
});
it('it will make no change if options.before is not found in the original', function(){
var toInsert = 'blahzorz blammo';
var originalContent = 'the original content';
var filePath = path.join(project.root, filename);
fs.writeFileSync(filePath, originalContent, { encoding: 'utf8' });
return blueprint.insertIntoFile(filename, toInsert, {before: 'not found' + EOL})
.then(function(result) {
var contents = fs.readFileSync(path.join(project.root, filename), { encoding: 'utf8' });
assert.equal(contents, originalContent, 'original content is unchanged');
assert.equal(result.originalContents, originalContent, 'returned object should contain original contents');
assert(!result.inserted, 'inserted should indicate that the file was not modified');
});
});
});
describe('lookupBlueprint', function() {
var blueprint;
var ui;
var tmpdir;
var project;
var filename;
beforeEach(function() {
tmpdir = tmp.in(tmproot);
blueprint = new Blueprint(basicBlueprint);
ui = new MockUI();
project = new MockProject();
// normally provided by `install`, but mocked here for testing
project.root = tmpdir;
blueprint.project = project;
project.blueprintLookupPaths = function() {
return [fixtureBlueprints];
};
filename = 'foo-bar-baz.txt';
});
afterEach(function() {
return rimraf(tmproot);
});
it('can lookup other Blueprints from the project blueprintLookupPaths', function() {
var result = blueprint.lookupBlueprint('basic_2');
assert.equal(result.description, 'Another basic blueprint');
});
it('can find internal blueprints', function() {
var result = blueprint.lookupBlueprint('controller');
assert.equal(result.description, 'Generates a controller of the given type.');
});
});
});