From 19c59a11d4db71159b2812b5215f44d272e64f2c Mon Sep 17 00:00:00 2001 From: s-hoff Date: Tue, 10 Jan 2017 23:36:23 +0100 Subject: [PATCH] feat(tests): Enabled unit testing --- lib/cli.js | 6 +- lib/file-system.js | 21 ++++ lib/project-item.js | 86 ++++++++------- lib/project.js | 4 +- package.json | 9 +- spec/helpers/polyfills.js | 1 + spec/lib/cli.spec.js | 190 ++++++++++++++++++++++++++++++++++ spec/lib/file-system.spec.js | 167 ++++++++++++++++++++++++++++++ spec/lib/project-item.spec.js | 95 +++++++++++++++++ spec/lib/project.spec.js | 67 ++++++++++++ spec/support/jasmine.json | 11 ++ 11 files changed, 606 insertions(+), 51 deletions(-) create mode 100644 spec/helpers/polyfills.js create mode 100644 spec/lib/cli.spec.js create mode 100644 spec/lib/file-system.spec.js create mode 100644 spec/lib/project-item.spec.js create mode 100644 spec/lib/project.spec.js create mode 100644 spec/support/jasmine.json diff --git a/lib/cli.js b/lib/cli.js index 127d8c171..48dd6bc55 100644 --- a/lib/cli.js +++ b/lib/cli.js @@ -95,7 +95,7 @@ function determineWorkingDirectory(dir) { return; } - return fs.exists(path.join(dir, 'aurelia_project')).then(result => { - return result ? dir : determineWorkingDirectory(parent); - }); + return fs.stat(path.join(dir, 'aurelia_project')) + .then(() => dir) + .catch(() => determineWorkingDirectory(parent)); } diff --git a/lib/file-system.js b/lib/file-system.js index bfc9bd533..91cc328e1 100644 --- a/lib/file-system.js +++ b/lib/file-system.js @@ -3,10 +3,31 @@ const fs = require('fs'); const nodePath = require('path'); const mkdirp = require('./mkdirp').mkdirp; +exports.fs = fs; + +/** + * @deprecated + * fs.exists() is deprecated. + * See https://nodejs.org/api/fs.html#fs_fs_exists_path_callback. + * Functions using it can also not be properly tested. + */ exports.exists = function(path) { return new Promise((resolve, reject) => fs.exists(path, resolve)); }; +exports.stat = function(path) { + return new Promise((resolve, reject) => { + fs.stat(path, (error, stats) => { + if(error) reject(error); + else resolve(stats); + }); + }); +}; + +exports.existsSync = function(path) { + return fs.existsSync(path); +}; + exports.mkdir = function(path) { return new Promise((resolve, reject) => { fs.mkdir(path, (error, result) => { diff --git a/lib/project-item.js b/lib/project-item.js index 16ad08125..d9d2f2318 100644 --- a/lib/project-item.js +++ b/lib/project-item.js @@ -92,12 +92,12 @@ exports.ProjectItem = class { let fullPath = relativeTo ? path.posix.join(relativeTo, this.name) : this.name; if (this.isDirectory) { - return fs.exists(fullPath).then(result => { - let dirReady = result ? Promise.resolve() : fs.mkdir(fullPath); - return dirReady.then(() => { - return Promise.all(this.children.map(child => child.create(ui, fullPath))); - }); - }); + return fs.stat(fullPath) + .then(result => result) + .catch(() => fs.mkdir(fullPath)) + .then(() => Promise.all(this.children.map(child => + child.create(ui, fullPath) + ))); } else if (this.sourcePath) { return fs.readFile(this.sourcePath).then(data => { return this._write(fullPath, data, ui); @@ -116,48 +116,44 @@ exports.ProjectItem = class { content = this.transformers[i](content); } - return fs.exists(fullPath).then(result => { - if (result) { - switch (this._fileExistsStrategy) { - case 'skip': - return Promise.resolve(); - case 'merge': - if (this.name = 'package.json') { - return fs.readFile(fullPath).then(data => { - let json = JSON.parse(data.toString()); - let merged = mergePackageJson(json, this.jsonObject); - return fs.writeFile(fullPath, JSON.stringify(merged, null, 2)); - }); - } else { - throw new Error(`cannot merge ${this.name}`); - } - case 'ask': - let question = `An existing file named '${this.name}' was found. What would you like to do?`; - let options = [ - { - displayName: 'Keep It', - description: 'Keeps your existing file. You may need to update its contents to work with Aurelia.' - }, - { - displayName: "Replace It", - description: "Replaces the existing file with a new one designed for Aurelia." - } - ]; - - return ui.question(question, options).then(answer => { - if (answer == options[0]) { - return Promise.resolve(); - } - - return fs.writeFile(fullPath, content); + return fs.stat(fullPath).then(() => { + switch (this._fileExistsStrategy) { + case 'skip': + return Promise.resolve(); + case 'merge': + if (this.name = 'package.json') { + return fs.readFile(fullPath).then(data => { + let json = JSON.parse(data.toString()); + let merged = mergePackageJson(json, this.jsonObject); + return fs.writeFile(fullPath, JSON.stringify(merged, null, 2)); }); - default: + } else { + throw new Error(`cannot merge ${this.name}`); + } + case 'ask': + let question = `An existing file named '${this.name}' was found. What would you like to do?`; + let options = [ + { + displayName: 'Keep It', + description: 'Keeps your existing file. You may need to update its contents to work with Aurelia.' + }, + { + displayName: "Replace It", + description: "Replaces the existing file with a new one designed for Aurelia." + } + ]; + + return ui.question(question, options).then(answer => { + if (answer == options[0]) { + return Promise.resolve(); + } + return fs.writeFile(fullPath, content); - } + }); + default: + return fs.writeFile(fullPath, content); } - - return fs.writeFile(fullPath, content); - }); + }).catch(() => fs.writeFile(fullPath, content)); } static jsonObject(name, jsonObject) { diff --git a/lib/project.js b/lib/project.js index f61a016d5..f285a399f 100644 --- a/lib/project.js +++ b/lib/project.js @@ -71,12 +71,12 @@ exports.Project = class { resolveGenerator(name) { let potential = path.join(this.generatorDirectory, `${name}${this.model.transpiler.fileExtension}`); - return fs.exists(potential).then(result => result ? potential : null); + return fs.stat(potential).then(() => potential).catch(() => null); } resolveTask(name) { let potential = path.join(this.taskDirectory, `${name}${this.model.transpiler.fileExtension}`); - return fs.exists(potential).then(result => result ? potential : null); + return fs.stat(potential).then(() => potential).catch(() => null); } } diff --git a/package.json b/package.json index acde9e516..619b31e82 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,10 @@ "aurelia": "bin/aurelia-cli.js", "au": "bin/aurelia-cli.js" }, + "scripts": { + "test": "jasmine", + "test:watch": "nodemon -x 'npm test'" + }, "license": "MIT", "author": "Rob Eisenberg (http://robeisenberg.com/)", "main": "lib/index.js", @@ -29,6 +33,9 @@ "npm": "^3.10.8" }, "devDependencies": { - "aurelia-tools": "^0.2.2" + "aurelia-tools": "^0.2.2", + "jasmine": "^2.5.2", + "mock-fs": "^4.2.0", + "nodemon": "^1.11.0" } } diff --git a/spec/helpers/polyfills.js b/spec/helpers/polyfills.js new file mode 100644 index 000000000..1f0431356 --- /dev/null +++ b/spec/helpers/polyfills.js @@ -0,0 +1 @@ +require('aurelia-polyfills'); diff --git a/spec/lib/cli.spec.js b/spec/lib/cli.spec.js new file mode 100644 index 000000000..cde3ee6ee --- /dev/null +++ b/spec/lib/cli.spec.js @@ -0,0 +1,190 @@ +"use strict"; +describe('The cli', () => { + let fs; + let path; + let mockfs; + let cli; + let Project; + let project; + + let dir; + let aureliaProject; + + beforeEach(() => { + fs = require('../../lib/file-system'); + path = require('path'); + cli = new (require('../../lib/cli').CLI)(); + Project = require('../../lib/project').Project; + mockfs = require('mock-fs'); + project = {}; + + dir = 'workspaces'; + aureliaProject = 'aurelia_project'; + const fsConfig = {}; + fsConfig[dir] = {}; + fsConfig['package.json'] = {}; + + mockfs(fsConfig); + }); + + afterEach(() => { + mockfs.restore(); + }); + + describe('The _establishProject() function', () => { + let establish; + + beforeEach(() => { + establish = spyOn(Project, 'establish').and.returnValue(project); + }); + + it('resolves to nothing', done => { + cli._establishProject({}) + .then(project => { + expect(project).not.toBeDefined(); + }) + .catch(fail).then(done); + }); + + it('calls and resolves to Project.establish()', done => { + fs.mkdirp(path.join(process.cwd(), aureliaProject)) + .then(() => cli._establishProject({ + runningLocally: true + })) + .then(project => { + expect(Project.establish) + .toHaveBeenCalledWith(cli.ui, path.join(process.cwd())); + expect(project).toBe(project); + }) + .catch(fail).then(done); + }); + + it('does not catch Project.establish()', done => { + establish.and.callFake(() => new Promise((resolve, reject) => reject())); + + fs.mkdirp(path.join(process.cwd(), aureliaProject)) + .then(() => cli._establishProject({ + runningLocally: true + })) + .then(() => { + fail('expected promise to be rejected.'); + done(); + }) + .catch(done); + }); + + it(`logs 'No Aurelia project found.'`, done => { + spyOn(cli.ui, 'log'); + cli._establishProject({ + runningLocally: true + }).then(() => { + expect(cli.ui.log).toHaveBeenCalledWith('No Aurelia project found.'); + }).catch(fail).then(done); + }); + }); + + describe('The createHelpCommand() function', () => { + it('gets the help command', () => { + mockfs({ + 'lib/commands/help/command.js': 'module.exports = {}' + }); + spyOn(cli.container, 'get'); + + cli.createHelpCommand(); + expect(cli.container.get) + .toHaveBeenCalledWith(require('../../lib/commands/help/command')); + }); + }); + + describe('The configureContainer() function', () => { + it('registers the instances', () => { + const registerInstanceSpy = spyOn(cli.container, 'registerInstance'); + + cli.configureContainer(); + + expect(registerInstanceSpy.calls.count()).toBe(2); + }); + }); + + describe('The run() function', () => { + function getVersionSpec(command) { + return () => { + beforeEach(() => { + mockfs({ + 'package.json': '{"version": "1.0.0"}' + }) + spyOn(cli.ui, 'log') + .and.callFake(() => new Promise(resolve => resolve())); + }); + + it('logs the cli version', () => { + cli.run(command); + expect(cli.ui.log).toHaveBeenCalledWith('1.0.0'); + }); + + it('returns an empty promise', done => { + cli.run(command).then(resolved => { + expect(resolved).not.toBeDefined(); + }).catch(fail).then(done); + }); + }; + } + + describe('The --version arg', getVersionSpec('--version')); + + describe('The -v arg', getVersionSpec('-v')); + + it('uses the _establishProject() function', done => { + // const project = {}; + spyOn(cli, '_establishProject').and.returnValue(new Promise(resolve => { + resolve(project); + })); + spyOn(cli.container, 'registerInstance'); + spyOn(cli, 'createCommand').and.returnValue({ execute: () => {} }); + + cli.run() + .then(() => { + expect(cli._establishProject).toHaveBeenCalledWith(cli.options); + }).catch(fail).then(done); + }); + + it('registers the project instance', done => { + spyOn(cli, '_establishProject').and.returnValue(new Promise(resolve => { + resolve(project); + })); + spyOn(cli.container, 'registerInstance'); + spyOn(cli, 'createCommand').and.returnValue({ execute: () => {} }); + + cli.run().then(() => { + expect(cli.container.registerInstance) + .toHaveBeenCalledWith(Project, project); + }).catch(fail).then(done); + }); + + it('creates the command', done => { + const command = 'run'; + const args = {}; + spyOn(cli, 'createCommand').and.returnValue({ execute: () => {} }); + + cli.run(command, args).then(() => { + expect(cli.createCommand).toHaveBeenCalledWith(command, args); + }).catch(fail).then(done); + }); + + it('executes the command', done => { + const command = { + execute: () => {} + }; + const args = {}; + spyOn(cli, '_establishProject').and.returnValue(new Promise(resolve => + resolve(project) + )); + spyOn(command, 'execute').and.returnValue(new Promise(resolve => resolve({}))); + spyOn(cli, 'createCommand').and.returnValue(command); + + cli.run('run', args).then(() => { + expect(command.execute).toHaveBeenCalledWith(args); + }).catch(fail).then(done); + }); + }); +}); \ No newline at end of file diff --git a/spec/lib/file-system.spec.js b/spec/lib/file-system.spec.js new file mode 100644 index 000000000..bacf4b0ca --- /dev/null +++ b/spec/lib/file-system.spec.js @@ -0,0 +1,167 @@ +"use strict"; + +const ERROR_CODES = { + ENOENT: 'ENOENT', + EEXIST: 'EEXIST' +}; + +describe('The file-system module', () => { + let mockfs; + let path; + let mkdirp; + let fs; + + let readDir; + let readFile; + let writeDir; + let writeFile; + + beforeEach(() => { + mockfs = require('mock-fs'); + path = require('path'); + mkdirp = require('../../lib/mkdirp'); + fs = require('../../lib/file-system'); + + readDir = 'read'; + readFile = { + name: 'read.js', + content: 'content' + }; + readFile.path = path.join(readDir, readFile.name); + + writeDir = 'write'; + writeFile = { + name: 'write.js', + content: 'content' + }; + writeFile.path = path.join(writeDir, writeFile.name); + + const config = {}; + config[readFile.path] = readFile.content; + + mockfs(config); + }); + + afterEach(() => { + mockfs.restore(); + }); + + describe('The stat() function', () => { + it('reads the stats for a directory', done => { + fs.stat(readDir).then(stats => { + expect(stats).toBeDefined(); + }).catch(fail).then(done); + }); + + it('reads the stats for a file', done => { + fs.stat(readFile.path).then(stats => { + expect(stats).toBeDefined(); + }).catch(fail).then(done); + }); + + it('rejects with an ENOENT error on a non-existing directory', done => { + fs.stat(writeDir).then(() => { + fail('expected promise to be rejected'); + }).catch(e => { + expect(e.code).toBe(ERROR_CODES.ENOENT); + }).then(done); + }); + + it('rejects with an ENOENT error on a non-existing file', done => { + fs.stat(writeFile.path).then(() => { + fail('expected promise to be rejected'); + }).catch(e => { + expect(e.code).toBe(ERROR_CODES.ENOENT); + }).then(done); + }) + }); + + describe('The readdir() function', () => { + it('reads a directory', done => { + fs.readdir(readDir).then(files => { + expect(files).toEqual([readFile.name]); + }).catch(fail).then(done); + }); + + it('rejects with ENOENT', done => { + fs.readdir(writeDir).then(() => { + fail('expected promise to be rejected'); + }).catch(e => { + expect(e.code).toBe(ERROR_CODES.ENOENT); + }).then(done); + }); + }); + + describe('The mkdir() function', () => { + it('makes a directory', done => { + fs.mkdir(writeDir) + .catch(fail) + .then(() => fs.readdir(writeDir)) + .catch(fail) + .then(done); + }); + + it('rejects with EEXIST', done => { + fs.mkdir(readDir) + .then(() => fail('expected promise to be rejected')) + .catch(e => expect(e.code).toBe(ERROR_CODES.EEXIST)) + .then(done); + }); + }); + + describe('The mkdirp() function', () => { + it('makes deep directories', done => { + fs.mkdirp(writeDir + readDir).then(() => { + return fs.readdir(writeDir + readDir); + }).catch(fail).then(done); + }); + + it('rejects if mkdirp returns an error', done => { + pending(`change file-system to mkdirp.mkdirp or more helpers.`); + }); + }); + + describe('The readFile() function', () => { + it('returns a promise resolving to the files content', done => { + fs.readFile(readFile.path).then(content => { + expect(content).toBe(readFile.content); + }).catch(fail).then(done); + }); + + it('rejects with ENOENT error', done => { + fs.readFile(writeFile.path).then(() => { + fail('expected promise to be rejected'); + }).catch(e => { + expect(e.code).toBe(ERROR_CODES.ENOENT); + done(); + }); + }); + }); + + describe('The readFileSync() function', () => { + it('returns the files content', () => { + expect(fs.readFileSync(readFile.path)) + .toBe(readFile.content); + }); + + it('throws an ENOENT error', () => { + try { + fs.readFileSync(writeFile.path); + fail(`expected fs.readFileSync('${writeFile.path}') to throw`); + } catch (e) { + expect(e.code).toBe(ERROR_CODES.ENOENT); + } + }); + }); + + describe('The writeFile() function', () => { + it('creates a new file', done => { + fs.writeFile(writeFile.path, writeFile.content).then(() => { + return fs.readFile(writeFile.path) + }).then(content => { + expect(content).toBe(writeFile.content); + done(); + }); + }); + }); +}); diff --git a/spec/lib/project-item.spec.js b/spec/lib/project-item.spec.js new file mode 100644 index 000000000..179ecc21d --- /dev/null +++ b/spec/lib/project-item.spec.js @@ -0,0 +1,95 @@ +describe('The project-item module', () => { + let mockfs; + + let fs; + + let ProjectItem; + let project; + + beforeEach(() => { + mockfs = require('mock-fs'); + + fs = require('../../lib/file-system'); + + ProjectItem = require('../../lib/project-item').ProjectItem; + + mockfs(); + }); + + afterEach(() => { + mockfs.restore(); + }); + + describe('The create() function', () => { + beforeEach(() => { + project = new ProjectItem(); + }); + + describe('isDirectory = true', () => { + beforeEach(() => { + project.isDirectory = true; + }); + + it('creates a directory if it is missing', done => { + project.name = 'cli-app'; + project.create() + .then(() => fs.readdir(project.name)) + .then(files => { + expect(files).toBeDefined(); + }).catch(fail).then(done); + }); + + it('creates the childs', done => { + const ui = {}; + const child = { create: () => { } }; + project.children.push(child); + spyOn(child, 'create'); + + project.name = 'cli-app'; + project.create(ui) + .then(() => { + expect(child.create).toHaveBeenCalledWith(ui, project.name); + }) + .catch(fail).then(done); + }); + }); + }); + + describe('The _write() function', () => { + beforeEach(() => { + project = new ProjectItem(); + }); + it('creates non-existing files', done => { + const file = { + path: 'index.html', + content: '', + }; + + project._write(file.path, file.content) + .then(() => fs.readFile(file.path)) + .then(content => { + expect(content).toBe(file.content); + }).catch(fail).then(done); + }); + + describe('in `skip` strategy', () => { + beforeEach(() => { + project._fileExistsStrategy = 'skip'; + }); + + it('does not override an existing file', done => { + const file = { + path: 'index.html', + content: '' + }; + + fs.writeFile(file.path, file.content) + .then(() => project._write(file.path, 'evil')) + .then(() => fs.readFile(file.path)) + .then(content => { + expect(content).toBe(file.content); + }).catch(fail).then(done); + }); + }); + }); +}); \ No newline at end of file diff --git a/spec/lib/project.spec.js b/spec/lib/project.spec.js new file mode 100644 index 000000000..b1e925fd5 --- /dev/null +++ b/spec/lib/project.spec.js @@ -0,0 +1,67 @@ +describe('The project module', () => { + let mockfs; + let path; + + let fs; + let ui; + + let Project; + let project; + + beforeEach(() => { + mockfs = require('mock-fs'); + path = require('path'); + + fs = require('../../lib/file-system'); + ui = new (require('../../lib/ui').ConsoleUI)(); + + Project = require('../../lib/project').Project; + + mockfs(); + + project = new Project(ui, '', { + paths: { }, + transpiler: { + fileExtension: '.js' + } + }); + }); + + afterEach(() => { + mockfs.restore(); + }); + + describe('The resolveGenerator() function', () => { + it('resolves to teh generators location', done => { + fs.writeFile('aurelia_project/generators/test.js', '') + .then(() => project.resolveGenerator('test')) + .then(location => { + expect(location).toBe(path.join('aurelia_project', 'generators', 'test.js')); + }).catch(fail).then(done); + }); + + it('resolves to null', done => { + project.resolveGenerator('test') + .then(location => { + expect(location).toBe(null); + }).catch(fail).then(done); + }); + }); + + describe('The resolveTask() function', () => { + it('resolves to the tasks location', done => { + fs.writeFile('aurelia_project/tasks/test.js', '') + .then(() => project.resolveTask('test')) + .then(location => { + expect(location).toBe(path.join('aurelia_project', 'tasks', 'test.js')); + }).catch(fail).then(done); + }); + + it('resolves to null', done => { + project.resolveTask('test') + .then(location => { + expect(location).toBe(null); + }).catch(fail).then(done); + }); + }); +}); \ No newline at end of file diff --git a/spec/support/jasmine.json b/spec/support/jasmine.json new file mode 100644 index 000000000..3ea316690 --- /dev/null +++ b/spec/support/jasmine.json @@ -0,0 +1,11 @@ +{ + "spec_dir": "spec", + "spec_files": [ + "**/*[sS]pec.js" + ], + "helpers": [ + "helpers/**/*.js" + ], + "stopSpecOnExpectationFailure": false, + "random": false +}