diff --git a/.babelrc b/.babelrc new file mode 100644 index 0000000..c1400f1 --- /dev/null +++ b/.babelrc @@ -0,0 +1,28 @@ +{ + "presets": [ + ["@babel/preset-env", { + "targets": { "node": true }, + "modules": "commonjs" + }] + ], + "plugins": [ + ["module-resolver", { + "alias": { + "commands": "./lib/commands", + "common": "./lib/common", + "meta": "./lib/meta", + "archetypes": "./lib/archetypes", + "utils": "./lib/utils" + } + }], + ["@babel/plugin-proposal-class-properties"], + ["@babel/plugin-transform-shorthand-properties"], + ["@babel/plugin-syntax-decorators", { "decoratorsBeforeExport": true }], + ["@babel/plugin-proposal-decorators", { "decoratorsBeforeExport": true }] + ], + "env": { + "test": { + "plugins": ["istanbul"] + } + } +} diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 0000000..9b3d213 --- /dev/null +++ b/.eslintignore @@ -0,0 +1,4 @@ +# Ignore linting +node_modules +bin +coverage diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000..0c038c4 --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,52 @@ +{ + "env": { + "es6": true, + "mocha": true + }, + "extends": [ + "standard", + "plugin:@typescript-eslint/eslint-recommended", + "plugin:@typescript-eslint/recommended" + ], + "globals": { + "assert": true, + "sinon": true, + "sandbox": true, + "wrap": true, + "register": true, + "initialize": true, + "expose": true, + "Atomics": "readonly", + "SharedArrayBuffer": "readonly" + }, + "plugins": [ + "import", + "mocha", + "promise", + "chai-assert-bdd", + "@typescript-eslint" + ], + "parser": "@typescript-eslint/parser", + "parserOptions": { + "ecmaVersion": 2020, + "sourceType": "module" + }, + "rules": { + "semi": ["error", "always"], + "indent": ["error", "tab"], + "no-tabs": ["error", { "allowIndentationTabs": true }], + "no-spaced-func": "off", + "space-before-function-paren": "off", + "no-trailing-spaces": ["error", { "skipBlankLines": true }], + "lines-between-class-members": ["error", "always", { "exceptAfterSingleLine": true }], + "new-cap": ["error", { "newIsCap": false, "capIsNew": false }], + "one-var": [2, { "uninitialized": "always" }], + "no-use-before-define": ["error", { "variables": false, "functions": true, "classes": true }], + "@typescript-eslint/triple-slash-reference": ["error", { "path": "always", "types": "always", "lib": "never" }], + "@typescript-eslint/no-explicit-any": "off", + "@typescript-eslint/explicit-function-return-type": "off", + "@typescript-eslint/no-use-before-define": "off", + "@typescript-eslint/no-empty-function": ["error", { "allow": ["functions", "arrowFunctions", "methods"] }], + "operator-linebreak": "off" + } +} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..ebf498c --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,24 @@ +name: CI Branches + +on: + push: + branches-ignore: + - 'master' + +jobs: + test: + name: Unit Testing + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v2 + + - name: Setup Node 10.x + uses: actions/setup-node@v1 + with: + node-version: '10.x' + + - name: Unit Testing + run: | + npm install + npm run test-sr diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 0000000..c3c4b0c --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,33 @@ +name: CI Deployment + +on: + pull_request: + types: [closed] + branches: + - master + +jobs: + deploy: + name: Deployment + runs-on: ubuntu-latest + if: github.event.pull_request.merged + + steps: + - name: Checkout + if: github.event.pull_request.merged + uses: actions/checkout@v2 + + - name: Use Node 10.x + uses: actions/setup-node@v1 + with: + node-version: 10.x + + - name: Unit Testing & Coverage + run: | + npm install + npm run coverage + + - name: Coveralls Report + uses: coverallsapp/github-action@master + with: + github-token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore index ad46b30..3a01126 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ +# Project + # Logs logs *.log @@ -11,51 +13,19 @@ pids *.seed *.pid.lock -# Directory for instrumented libs generated by jscoverage/JSCover -lib-cov - # Coverage directory used by tools like istanbul coverage - -# nyc test coverage -.nyc_output - -# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) -.grunt - -# Bower dependency directory (https://bower.io/) -bower_components - -# node-waf configuration -.lock-wscript - -# Compiled binary addons (https://nodejs.org/api/addons.html) -build/Release +.cache # Dependency directories node_modules/ -jspm_packages/ - -# TypeScript v1 declaration files -typings/ # Optional npm cache directory .npm -# Optional eslint cache -.eslintcache - -# Optional REPL history -.node_repl_history - -# Output of 'npm pack' -*.tgz - -# Yarn Integrity file -.yarn-integrity - -# dotenv environment variables file -.env +# IDEs +.idea +_babel_resolver.js -# next.js build output -.next +# OS +.DS_Store diff --git a/.mocharc.js b/.mocharc.js new file mode 100644 index 0000000..f1ba744 --- /dev/null +++ b/.mocharc.js @@ -0,0 +1,20 @@ +'use strict'; +require('@babel/register')({ cache: false }); + +/** + * Mocha Configuration + */ +module.exports = { + require: [ + 'json5/lib/register', + 'chai/register-assert', + './test/globals' + ], + ui: 'bdd', + diff: true, + extension: ['js', 'json5'], + 'inline-diffs': true, + timeout: 2000, + reporter: 'spec', + 'watch-files': ['lib/**/*.js', 'test/**/*.js'] +}; diff --git a/@types/core.d.ts b/@types/core.d.ts new file mode 100644 index 0000000..f361245 --- /dev/null +++ b/@types/core.d.ts @@ -0,0 +1,154 @@ +/** + * Synapse Core Typings + * @author Patricio Ferreira <3dimentionar@gmail.com> + * @module synapse-core + */ +declare module 'synapse-core' { + // Imports + import EventEmitter from 'events'; + import { StdioOptions } from 'child_process'; + import { Collection } from 'synapse-utils'; + + /** + * @type DependenciesType + */ + export type DependenciesType = 'dependencies' | 'devDependencies' | 'peerDependencies'; + + /** + * @type ScriptType + */ + export type ScriptType = + 'create' | + + 'start' | + + 'pre-config' | + 'config' | + 'post-config' | + + 'pre-clean' | + 'clean' | + 'post-clean' | + + 'pre-env' | + + 'pre-test' | + 'test' | + 'post-test' | + + 'pre-dev' | + 'dev' | + 'post-dev' | + + 'pre-prod' | + 'prod' | + 'post-prod' | + + 'post-env' | + + 'pre-serve' | + 'serve' | + 'post-serve' | + + 'pre-release' | + 'release' | + 'post-release' | + + 'end'; + + /** + * @type EnvironmentType + */ + export type EnvironmentType = 'all' | 'test' | 'dev' | 'prod'; + + /** + * @interface Core + * @extends EventEmitter + */ + export interface Core extends EventEmitter { + readonly defaults: Partial; + + attachEvents(): Core; + execute(methodName: string, ...args: any[]): any; + parse(attributes?: Partial): Core; + serialize(instance: any, ...omit: string[]): Partial; + } + + /** + * @interface Environment + * @extends Core + */ + export interface Environment extends Core { + readonly name: EnvironmentType | undefined; + readonly config: object | undefined; + + parse(attributes?: Partial): Environment; + parseName(attributes?: Partial): Environment; + parseConfig(attributes?: Partial, environment?: Environment): Environment; + } + + /** + * @interface Script + * @extends Core + */ + export interface Script extends Core { + readonly name: ScriptType | undefined; + readonly queue: Collection; + readonly stdio: StdioOptions; + + isSupported(name: string): boolean; + parse(attributes?: Partial + + + +
+ Version: {{ version }} + {% block scripts %}{% endblock %}} + + \ No newline at end of file diff --git a/test/integration/html/index.nunjucks b/test/integration/html/index.nunjucks new file mode 100644 index 0000000..6036ff7 --- /dev/null +++ b/test/integration/html/index.nunjucks @@ -0,0 +1,4 @@ +{% extends "./common/_master.nunjucks" %} + +{% block title %}Index Title{% endblock %} +{% block scripts % \ No newline at end of file diff --git a/test/integration/js/libraries/libraries-dev.ts b/test/integration/js/libraries/libraries-dev.ts new file mode 100644 index 0000000..cfca5ce --- /dev/null +++ b/test/integration/js/libraries/libraries-dev.ts @@ -0,0 +1,5 @@ +/** + * Development Third Party Libraries + */ +import 'plugin-typescript'; +import 'systemjs-hmr'; diff --git a/test/integration/js/libraries/libraries-prod.ts b/test/integration/js/libraries/libraries-prod.ts new file mode 100644 index 0000000..f2dadec --- /dev/null +++ b/test/integration/js/libraries/libraries-prod.ts @@ -0,0 +1,8 @@ +/** + * Production Third Party Libraries + */ +import 'backbone'; +import 'incremental-dom'; +import 'jquery'; +import 'systemjs'; +import 'underscore'; diff --git a/test/integration/js/libraries/libraries-test.ts b/test/integration/js/libraries/libraries-test.ts new file mode 100644 index 0000000..a134d46 --- /dev/null +++ b/test/integration/js/libraries/libraries-test.ts @@ -0,0 +1,5 @@ +/** + * Test Third Party Libraries + */ +import 'chai'; +import 'sinon'; diff --git a/test/integration/js/system.config.js b/test/integration/js/system.config.js new file mode 100644 index 0000000..f533675 --- /dev/null +++ b/test/integration/js/system.config.js @@ -0,0 +1,7 @@ +/** + * SystemJs Configuration + */ +SystemJS.config({ + baseURL: "./", + map: {} +}); \ No newline at end of file diff --git a/test/integration/package.json b/test/integration/package.json new file mode 100644 index 0000000..fe4933d --- /dev/null +++ b/test/integration/package.json @@ -0,0 +1,22 @@ +{ + "name": "synapse-project-sample", + "version": "1.0.0", + "description": "Project Example that uses synapse", + "private": true, + "scripts": { + "test": "synapse run test", + "dev": "synapse run dev", + "prod": "synapse run prod" + }, + "repository": { + "type": "git", + "url": "none" + }, + "keywords": [ + "synapse", + "project", + "sample" + ], + "author": "Patricio Ferreira <3dimentionar@gmail.com>", + "license": "UNLICENSED" +} diff --git a/test/integration/synapse.config.js b/test/integration/synapse.config.js new file mode 100644 index 0000000..5267b5d --- /dev/null +++ b/test/integration/synapse.config.js @@ -0,0 +1,73 @@ +module.exports = { + source: './src', + target: './public', + system: './js/system.config.js', + server: { + public: './public', + rewrites: [ + { + source: ':url', + destination: 'html/:url.html' + } + ], + cleanUrls: true, + port: 3000 + }, + plugins: [ + { + name: 'includepaths', + options: { + include: { + 'systemjs': './node_modules/systemjs/dist/s.js' + } + } + }, + { + name: 'node-resolve', + options: { + module: true, + jsnext: true, + main: true, + browser: true + } + }, + { + name: 'commonjs', + options: { + include: 'node_modules/**' + } + } + ], + html: { + path: './html', + srcExtension: '.nunjucks', + outExtension: '.html' + }, + test: { + libs: [ + 'js/libraries/libraries-test.ts', + 'js/libraries/libraries-dev.ts', + 'js/libraries/libraries-prod.ts' + ], + output: { + format: 'system' + } + }, + dev: { + libs: [ + 'js/libraries/libraries-dev.ts', + 'js/libraries/libraries-prod.ts' + ], + output: { + format: 'system' + } + }, + prod: { + libs: [ + 'js/libraries/libraries-prod.ts' + ], + output: { + format: 'system' + } + } +}; diff --git a/test/integration/tsconfig.json b/test/integration/tsconfig.json new file mode 100644 index 0000000..8c41395 --- /dev/null +++ b/test/integration/tsconfig.json @@ -0,0 +1,32 @@ +{ + "compilerOptions": { + "target": "es5", + "module": "system", + "lib": ["es5", "es6", "es2015", "es2016", "es2017","esnext", "dom"], + "allowJs": false, + "checkJs": false, + "sourceMap": true, + "removeComments": true, + "noEmit": true, + "importHelpers": true, + "downlevelIteration": true, + "strict": true, + "noImplicitAny": true, + "strictNullChecks": true, + "strictFunctionTypes": true, + "strictBindCallApply": true, + "noImplicitThis": true, + "alwaysStrict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "moduleResolution": "node", + "baseUrl": "./", + "types": ["./node_modules/@types"], + "allowSyntheticDefaultImports": true, + "esModuleInterop": true, + "inlineSourceMap": true, + "inlineSources": true, + "experimentalDecorators": true, + "emitDecoratorMetadata": true + } +} diff --git a/test/unit/commands/init_spec.js b/test/unit/commands/init_spec.js new file mode 100644 index 0000000..29bad05 --- /dev/null +++ b/test/unit/commands/init_spec.js @@ -0,0 +1,25 @@ +/** + * SynapseCommand Class Specs + * @module common + * @author Patricio Ferreira <3dimentionar@gmail.com> + */ +import { InitCommand } from 'commands/init'; + +describe.skip('class InitCommand', function() { + describe('constructor()', () => { + it('should instantiate class', () => { + this.command = new InitCommand(); + assert.instanceOf(this.command, InitCommand); + }); + }); + + describe('methods', () => { + beforeEach(() => { + this.mockCommand = sandbox.mock(this.command); + }); + + afterEach(() => { + sandbox.restore(); + }); + }); +}); diff --git a/test/unit/common/core/archetype_spec.js b/test/unit/common/core/archetype_spec.js new file mode 100644 index 0000000..2444dba --- /dev/null +++ b/test/unit/common/core/archetype_spec.js @@ -0,0 +1,8 @@ +/** + * Archetype Class Specs + * @module common.core + * @author Patricio Ferreira <3dimentionar@gmail.com> + */ +describe('Archetype Class', function() { + // TODO +}); diff --git a/test/unit/common/core/core_spec.js b/test/unit/common/core/core_spec.js new file mode 100644 index 0000000..82e4929 --- /dev/null +++ b/test/unit/common/core/core_spec.js @@ -0,0 +1,169 @@ +/** + * Core Specs + * @module common.core + * @author Patricio Ferreira <3dimentionar@gmail.com> + */ +import * as _ from 'underscore'; +import * as debug from 'utils/debug/debug'; +import * as utils from 'utils/utils'; +import colors from 'ansi-colors'; +import ux from 'cli-ux'; +import Instance, { Core } from 'common/core/core'; + +describe('Core Class', function() { + beforeEach(() => { + this.mDate = sandbox.mock(Date.prototype); + this.mConsole = sandbox.mock(console); + this.mError = new Error('Mock Fatal Error'); + this.mIsProduction = sandbox.stub(debug, 'isProduction'); + this.mUtils = sandbox.mock(utils); + this.mUxAction = sandbox.mock(ux.action); + this.aCommand = { + toString: () => { return '[SomeCommand]'; }, + exit: (code) => code + }; + this.mCommand = sandbox.mock(this.aCommand); + + this.bannerTemplate = _.template(Core.output.banner); + }); + + afterEach(() => { + sandbox.restore(); + }); + + describe('static#output', () => { + it('should retrieve synapse base configuration output', () => { + assert.isDefined(Core.output.synapse); + assert.isDefined(Core.output.synapse.source); + assert.isDefined(Core.output.synapse.target); + assert.isDefined(Core.output.banner); + assert.isDefined(Core.output.dependencies); + assert.isDefined(Core.output.devDependencies); + assert.isDefined(Core.output.peerDependencies); + }); + }); + + describe('static#getBannerDefaults()', () => { + it('should retrieve banner defaults: when year is 2020', () => { + const result = Core.getBannerDefaults(); + + assert.equal(result.version, 'v[?.?.?]'); + assert.equal(result.year, 2020); + }); + it('should retrieve banner defaults: when year is NOT 2020', () => { + const mockYear = 2021; + this.mDate.expects('getFullYear').once().returns(mockYear); + + const result = Core.getBannerDefaults(); + + assert.equal(result.version, 'v[?.?.?]'); + assert.equal(result.year, `2020-${mockYear}`); + this.mDate.verify(); + }); + }); + + describe('banner()', () => { + it('should output synapse banner to the stdin: when in production', () => { + const mockModel = { version: '1.1.0', url: 'http://myurl', year: 2020 }; + this.mIsProduction.returns(true); + this.mConsole.expects('log') + .once() + .withArgs(colors.cyan(this.bannerTemplate(mockModel))); + + Instance.banner(mockModel); + + this.mConsole.verify(); + }); + }); + + describe('onProgress()', () => { + it('should handle progress status: production is on, message & status are defined', () => { + this.mIsProduction.returns(true); + this.mUtils.expects('defined') + .twice() + .returns(true); + this.mUxAction.expects('start') + .once() + .withArgs('Some Message', 'Initializing', Instance.uxOptions); + + assert.equal(Instance.onProgress(this.aCommand, 'Some Message', 'Initializing'), this.aCommand); + assert.isTrue(this.mIsProduction.calledOnce); + + this.mUtils.verify(); + this.mUxAction.verify(); + }); + it('should handle progress status: production is on, message defined & status not defined', () => { + this.mIsProduction.returns(true); + this.mUtils.expects('defined').twice() + .onFirstCall().returns(true) + .onSecondCall().returns(false); + this.mUxAction.expects('start').never(); + + assert.equal(Instance.onProgress(this.aCommand, 'Some Message'), this.aCommand); + assert.isTrue(this.mIsProduction.calledOnce); + + this.mUtils.verify(); + this.mUxAction.verify(); + }); + it('should handle progress status: production is off, no message & status are defined', () => { + this.mIsProduction.returns(false); + this.mUtils.expects('defined').never(); + this.mUxAction.expects('start').never(); + + assert.equal(Instance.onProgress(this.aCommand, undefined, 'Initializing'), this.aCommand); + assert.isTrue(this.mIsProduction.calledOnce); + + this.mUtils.verify(); + this.mUxAction.verify(); + }); + }); + + describe('onSuccess()', () => { + it('should handle success status: when production is on', () => { + this.mIsProduction.returns(true); + this.mUxAction.expects('stop').once().withArgs(''); + + assert.equal(Instance.onSuccess(this.aCommand), this.aCommand); + assert.isTrue(this.mIsProduction.calledOnce); + + this.mUxAction.verify(); + }); + it('should handle success status: when production is off', () => { + this.mIsProduction.returns(false); + this.mUxAction.expects('stop').never(); + + assert.equal(Instance.onSuccess(this.aCommand), this.aCommand); + assert.isTrue(this.mIsProduction.calledOnce); + + this.mUxAction.verify(); + }); + }); + + describe('onError()', () => { + it('should handle error status: when production is on', () => { + const options = { code: 1 }; + this.mIsProduction.returns(true); + this.mUxAction.expects('stop').withArgs(`${this.mError.message} - Code: ${options.code}`); + this.mCommand.expects('exit').once().withArgs(options.code); + + assert.isUndefined(Instance.onError(this.mError, this.aCommand, options)); + assert.isTrue(this.mIsProduction.calledOnce); + + this.mCommand.verify(); + this.mUxAction.verify(); + }); + it('should handle error status: when production is off', () => { + const options = { code: 1 }; + this.mIsProduction.returns(false); + this.mUxAction.expects('stop').never(); + this.mCommand.expects('exit').once().withArgs(options.code); + + assert.isUndefined(Instance.onError(this.mError, this.aCommand, options)); + assert.isTrue(this.mIsProduction.calledOnce); + + this.mCommand.verify(); + this.mUxAction.verify(); + }); + }); + +}); diff --git a/test/unit/common/core/normalizer_spec.js b/test/unit/common/core/normalizer_spec.js new file mode 100644 index 0000000..7ba6fc1 --- /dev/null +++ b/test/unit/common/core/normalizer_spec.js @@ -0,0 +1,8 @@ +/** + * Normalizer Class Specs + * @module common.core + * @author Patricio Ferreira <3dimentionar@gmail.com> + */ +describe('Normalizer Class', function() { + // TODO +}); diff --git a/test/unit/common/pipeline-command_spec.js b/test/unit/common/pipeline-command_spec.js new file mode 100644 index 0000000..9ba51a0 --- /dev/null +++ b/test/unit/common/pipeline-command_spec.js @@ -0,0 +1,89 @@ +/** + * PipelineCommand Class Specs + * @module common + * @author Patricio Ferreira <3dimentionar@gmail.com> + */ +import { PipelineCommand } from 'common/pipeline-command'; +import Collection from 'utils/adt/collection'; +import * as debug from 'utils/debug/debug'; + +describe.skip('class PipelineCommand', function() { + describe('constructor()', () => { + it('should instantiate class', () => { + this.command = new PipelineCommand(); + assert.instanceOf(this.command, PipelineCommand); + }); + }); + + describe('methods', () => { + beforeEach(() => { + this.mIsProduction = sandbox.stub(debug, 'isProduction'); + this.mCommand = sandbox.mock(this.command); + }); + + afterEach(() => { + sandbox.restore(); + }); + + describe('tasks', () => { + it('should retrieve the list of tasks associated with this command', () => { + const result = this.command.tasks; + assert.instanceOf(result, Collection); + assert.equal(result.size(), 8); + assert.include(result, this.command.start); + assert.include(result, this.command.load); + assert.include(result, this.command.configuration); + assert.include(result, this.command.clean); + assert.include(result, this.command.env); + assert.include(result, this.command.process); + assert.include(result, this.command.release); + assert.include(result, this.command.end); + }); + }); + + describe('configuration()', () => { + it('should execute configuration', async () => { + assert.equal(await this.command.configuration(), this.command); + }); + }); + + describe('clean()', () => { + it('should execute clean', async () => { + assert.equal(await this.command.clean(), this.command); + }); + }); + + describe('env()', () => { + it('should execute env', async () => { + assert.equal(await this.command.env(), this.command); + }); + }); + + describe('process()', () => { + it('should execute process', async () => { + assert.equal(await this.command.process(), this.command); + }); + }); + + describe('release()', () => { + it('should execute release', async () => { + assert.equal(await this.command.release(), this.command); + }); + }); + + describe('run()', () => { + it('should run the command', async () => { + const result = await this.command.run(); + assert.isArray(result); + assert.lengthOf(result, 8); + assert.include(result, this.command); + }); + }); + + describe('toString()', () => { + it('should returns a string representation of the instance', () => { + assert.equal(this.command.toString(), `[${PipelineCommand.name}]`); + }); + }); + }); +}); diff --git a/test/unit/common/scaffold-command_spec.js b/test/unit/common/scaffold-command_spec.js new file mode 100644 index 0000000..56250a2 --- /dev/null +++ b/test/unit/common/scaffold-command_spec.js @@ -0,0 +1,112 @@ +/** + * ScaffoldCommand Class Specs + * @module common + * @author Patricio Ferreira <3dimentionar@gmail.com> + */ +import { flags } from '@oclif/command'; +import { SynapseCommand } from 'common/synapse-command'; +import { ScaffoldCommand } from 'common/scaffold-command'; +import * as debug from 'utils/debug/debug'; +import Collection from 'utils/adt/collection'; + +describe('class ScaffoldCommand', function() { + beforeEach(() => { + this.mSuper = sandbox.mock(SynapseCommand.prototype); + ScaffoldCommand.flags = { + test: flags.string({ + char: 't', + description: 'Test description', + hidden: false, + env: 'SYNAPSE', + options: ['A', 'B', 'C'], + default: null, + required: false + }) + }; + }); + + afterEach(() => { + sandbox.restore(); + }); + + describe('constructor()', () => { + it('should instantiate class', () => { + this.command = new ScaffoldCommand([], {}); + this.command.init(); + assert.instanceOf(this.command, ScaffoldCommand); + }); + it('should verify decorator interface is applied to the command instance', () => { + assert.isOk(this.command.createQuestion); + assert.isOk(this.command.newPrompt); + assert.isOk(this.command.bindEvents); + }); + }); + + describe('methods', () => { + beforeEach(() => { + this.mIsProduction = sandbox.stub(debug, 'isProduction'); + this.mCommand = sandbox.mock(this.command); + this.mAnswers = sandbox.mock(this.command.answers); + + this.mQuestion = { name: 'test' }; + }); + + afterEach(() => { + sandbox.restore(); + }); + + describe('init()', () => { + it('should initialize command', async() => { + this.mSuper.expects('init') + .once() + .resolves(this.command); + this.mCommand.expects('attachEvents') + .once() + .returns(this.command); + assert.instanceOf(await this.command.init(), ScaffoldCommand); + }); + }); + + describe('attachEvents()', () => { + it('should attach events to the command', () => { + this.mAnswers.expects('on') + .once() + .withArgs(Collection.events.add, this.command._onCapture) + .returns(this.command); + assert.instanceOf(this.command.attachEvents(), ScaffoldCommand); + }); + }); + + describe('isFlagAvailable()', () => { + it('should return false: question.name is not defined', () => { + assert.isFalse(this.command.isFlagAvailable({})); + }); + it('should return false: flag with question.name does NOT exists', () => { + assert.isFalse(this.command.isFlagAvailable({ name: 'non-existent' })); + }); + it('should return true', () => { + assert.isTrue(this.command.isFlagAvailable(this.mQuestion)); + }); + }); + + describe('isFlagAvailable()', () => {}); + + describe('findAnswerByName()', () => {}); + + describe('question()', () => {}); + + describe('answer()', () => {}); + + describe('createPrompt()', () => {}); + + describe('prompt()', () => {}); + + describe('process()', () => {}); + + describe('toString()', () => { + it('should returns a string representation of the instance', () => { + assert.equal(this.command.toString(), `[${ScaffoldCommand.name}]`); + }); + }); + }); +}); diff --git a/test/unit/common/synapse-command_spec.js b/test/unit/common/synapse-command_spec.js new file mode 100644 index 0000000..f358a71 --- /dev/null +++ b/test/unit/common/synapse-command_spec.js @@ -0,0 +1,153 @@ +/** + * SynapseCommand Class Specs + * @module common + * @author Patricio Ferreira <3dimentionar@gmail.com> + */ +import { SynapseCommand } from 'common/synapse-command'; +import Core from 'common/core/core'; +import Collection from 'utils/adt/collection'; +import colors from 'ansi-colors'; + +describe('class SynapseCommand', function() { + beforeEach(() => { + this.mockConfig = { + config: { + pjson: { + version: '1.0.0' + } + } + }; + + this.mCommandClass = sandbox.mock(SynapseCommand.prototype); + this.mCore = sandbox.mock(Core); + }); + + afterEach(() => { + sandbox.restore(); + }); + + describe('constructor()', () => { + it('should instantiate class', () => { + this.mCommandClass.expects('parse') + .once() + .withArgs(SynapseCommand) + .returns(this.mockConfig); + this.command = new SynapseCommand([], { root: process.cwd() }); + this.command.init(); + assert.instanceOf(this.command, SynapseCommand); + }); + }); + + describe('methods', () => { + beforeEach(() => { + this.mCommand = sandbox.mock(this.command); + this.mStart = sandbox.mock(this.command.start); + this.mLoad = sandbox.mock(this.command.load); + this.mEnd = sandbox.mock(this.command.end); + this.mError = new Error('Mock Fatal Error'); + + this.spyCollectionInvoke = sandbox.spy(this.command.tasks, 'invoke'); + }); + + afterEach(() => { + sandbox.restore(); + }); + + describe('tasks', () => { + it('should retrieve the list of tasks associated with this command', () => { + const result = this.command.tasks; + assert.instanceOf(result, Collection); + assert.equal(result.size(), 3); + assert.include(result, this.command.start); + assert.include(result, this.command.load); + assert.include(result, this.command.end); + }); + }); + + describe('start()', () => { + it('should start the command', async () => { + this.mCore.expects('banner') + .once() + .withArgs({ version: `v${this.command.config.pjson.version}`, url: 'Documentation: http://nahuel.io/synapse' }) + .returns(null); + assert.equal(await this.command.start(), this.command); + + this.mCore.verify(); + }); + }); + + describe('load()', () => { + it('should load project package: package hasn\'t been loaded', async () => { + this.mCore.expects('onProgress') + .once() + .withArgs(this.command, 'Package Configuration', colors.yellow.bold('[Reading...]')) + .returns(this.command); + this.mCore.expects('onSuccess') + .once() + .withArgs(this.command, colors.green.bold('[Loaded]')) + .returns(this.command); + + assert.equal(await this.command.load(), this.command); + assert.isOk(this.command.package); + assert.include(this.command.package, { name: 'synapse' }); + + this.mCore.verify(); + }); + }); + + describe('end()', () => { + it('should end the command', async () => { + assert.equal(await this.command.end(), this.command); + }); + }); + + describe('run()', () => { + it('should run the command: success', async () => { + const stubTask = sandbox.stub().resolves(this.command); + this.mStart.expects('bind').once().returns(stubTask); + this.mLoad.expects('bind').once().returns(stubTask); + this.mEnd.expects('bind').once().returns(stubTask); + + const result = await this.command.run(); + + assert.isArray(result); + assert.lengthOf(result, 3); + assert.include(result, this.command); + + assert.isTrue(this.spyCollectionInvoke.calledOnce); + assert.isTrue(stubTask.calledThrice); + + this.mStart.verify(); + this.mLoad.verify(); + this.mEnd.verify(); + }); + it('should run the command: fail', async () => { + const stubTask = sandbox.stub() + .onFirstCall().resolves(this.command) + .onSecondCall().rejects(this.mError) + .onThirdCall().resolves(this.command); + this.mCore.expects('onError').once().withArgs(this.mError, this.command); + this.mStart.expects('bind').once().returns(stubTask); + this.mLoad.expects('bind').once().returns(stubTask); + this.mEnd.expects('bind').once().returns(stubTask); + + const result = await this.command.run(); + + assert.isUndefined(result); + assert.isTrue(this.spyCollectionInvoke.calledOnce); + assert.isTrue(stubTask.calledThrice); + + this.mCore.verify(); + this.mStart.verify(); + this.mLoad.verify(); + this.mEnd.verify(); + }); + }); + + describe('toString()', () => { + it('should returns a string representation of the instance', () => { + assert.equal(this.command.toString(), `[${SynapseCommand.name}]`); + }); + }); + }); +}); diff --git a/test/unit/utils/adt/collection_spec.js b/test/unit/utils/adt/collection_spec.js new file mode 100644 index 0000000..a91fe34 --- /dev/null +++ b/test/unit/utils/adt/collection_spec.js @@ -0,0 +1,453 @@ +/** + * Collection + * @module utils.adt + * @author Patricio Ferreira <3dimentionar@gmail.com> + */ +import extend from 'extend'; +import Collection from 'utils/adt/collection'; + +describe('class Collection', function() { + before(() => { + this.mPrimitives = [1, 2, 3]; + this.mElements = [{ message: 'E1', value: 1 }, { message: 'E2', value: 2 }, { message: 'E3', value: 3 }]; + this.Generic = class Generic { + constructor(attrs = {}) { extend(true, this, attrs); } + say(prefix) { return `${prefix} ${this.message}`; }; + toJSON() { return { message: this.message, value: this.value }; } + }; + }); + + describe('constructor()', () => { + it('should instantiate class: empty', () => { + this.empty = new Collection(); + assert.instanceOf(this.empty, Collection); + assert.equal(this.empty.size(), 0); + }); + it('should instantiate class: primitives', () => { + this.primitives = new Collection(this.mPrimitives); + assert.instanceOf(this.primitives, Collection); + assert.equal(this.primitives.size(), 3); + }); + it('should instantiate class: interface', () => { + this.elements = new Collection(this.mElements, { _interface: this.Generic }); + assert.instanceOf(this.elements, Collection); + assert.equal(this.elements.size(), 3); + }); + }); + + describe('methods', () => { + beforeEach(() => { + this.primitives.push(this.mPrimitives); + this.elements.push(this.mElements); + }); + + afterEach(() => { + this.primitives.removeAllListeners(); + this.elements.removeAllListeners(); + this.primitives.reset(); + this.elements.reset(); + sandbox.restore(); + }); + + describe('_fireEvent()', () => { + it('should NOT fire an event: name is not defined', () => { + this.spyEmit = sandbox.spy(this.primitives, 'emit'); + assert.equal(this.primitives._fireEvent(), this.primitives); + assert.isFalse(this.spyEmit.calledOnce); + }); + it('should NOT fire an event: name defined, options.silent = true', () => { + this.spyEmit = sandbox.spy(this.primitives, 'emit'); + assert.equal(this.primitives._fireEvent('some:event', { silent: true }), this.primitives); + assert.isFalse(this.spyEmit.calledOnce); + }); + }); + + describe('at()', () => { + it('should retrieve an element at a given index', () => { + assert.hasAllKeys(this.elements.at(2), ['message', 'value']); + }); + it('should retrieve an element at a given index: default (first element)', () => { + assert.hasAllKeys(this.elements.at(), ['message', 'value']); + }); + }); + + describe('push()', () => { + it('should add new element/s to the end of the collection (primitives)', (done) => { + this.primitives.on(Collection.events.add, (collection, elements, options) => { + assert.equal(collection, this.primitives); + assert.equal(collection.size(), 4); + assert.isArray(elements); + assert.lengthOf(elements, 1); + assert.isObject(options); + done(); + }); + assert.equal(this.primitives.push(1), this.primitives); + }); + it('should add new element/s to the end of the collection (interface)', (done) => { + this.elements.on(Collection.events.add, (collection, elements, options) => { + assert.equal(collection, this.elements); + assert.equal(collection.size(), 5); + assert.isArray(elements); + assert.lengthOf(elements, 2); + assert.isObject(options); + done(); + }); + assert.equal(this.elements.push([{ message: 'E4', value: 4 }, { message: 'E5', value: 5 }]), this.elements); + }); + }); + + describe('unshift()', () => { + it('should add new element/s to the beginning of the collection (primitives)', (done) => { + this.primitives.on(Collection.events.add, (collection, elements, options) => { + assert.equal(collection, this.primitives); + assert.equal(collection.size(), 5); + assert.isArray(elements); + assert.lengthOf(elements, 2); + assert.isObject(options); + done(); + }); + assert.equal(this.primitives.unshift([4, 5]), this.primitives); + }); + it('should add new element/s to the beginning of the collection (interface)', (done) => { + this.elements.on(Collection.events.add, (collection, elements, options) => { + assert.equal(collection, this.elements); + assert.equal(collection.size(), 4); + assert.isArray(elements); + assert.lengthOf(elements, 1); + assert.isObject(options); + done(); + }); + assert.equal(this.elements.unshift([{ message: 'E0', value: 0 }]), this.elements); + }); + }); + + describe('insert()', () => { + it('should insert new element/s at a given index (primitives)', (done) => { + this.primitives.on(Collection.events.add, (collection, elements, options) => { + assert.equal(collection, this.primitives); + assert.equal(collection.size(), 5); + assert.isArray(elements); + assert.lengthOf(elements, 2); + assert.isObject(options); + done(); + }); + assert.equal(this.primitives.insert([4, 5], 2), this.primitives); + }); + it('should insert new element/s at a given index (interface)', (done) => { + this.elements.on(Collection.events.add, (collection, elements, options) => { + assert.equal(collection, this.elements); + assert.equal(collection.size(), 5); + assert.isArray(elements); + assert.lengthOf(elements, 2); + assert.isObject(options); + done(); + }); + assert.equal(this.elements.insert([{ message: 'E4', value: 4 }, { message: 'E5', value: 5 }], 2), this.elements); + }); + }); + + describe('remove()', () => { + it('should remove existing elements (primitives)', (done) => { + this.primitives.on(Collection.events.remove, (collection, removed, options) => { + assert.equal(collection, this.primitives); + assert.equal(collection.size(), 1); + assert.isArray(removed); + assert.lengthOf(removed, 2); + assert.isObject(options); + done(); + }); + assert.equal(this.primitives.remove([2, 3]), this.primitives); + }); + it('should remove existing elements (interface)', (done) => { + this.elements.on(Collection.events.remove, (collection, removed, options) => { + assert.equal(collection, this.elements); + assert.equal(collection.size(), 1); + assert.isArray(removed); + assert.lengthOf(removed, 2); + assert.isObject(options); + done(); + }); + assert.equal(this.elements.remove([{ message: 'E1', value: 1 }, { message: 'E3', value: 3 }]), this.elements); + }); + }); + + describe('pop()', () => { + it('should remove last element (primitives)', (done) => { + this.primitives.on(Collection.events.remove, (collection, removed, options) => { + assert.equal(collection, this.primitives); + assert.equal(collection.size(), 2); + assert.isArray(removed); + assert.lengthOf(removed, 1); + assert.isObject(options); + done(); + }); + assert.equal(this.primitives.pop(), this.primitives); + }); + }); + + describe('shift()', () => { + it('should remove first element (primitives)', (done) => { + this.primitives.on(Collection.events.remove, (collection, removed, options) => { + assert.equal(collection, this.primitives); + assert.equal(collection.size(), 2); + assert.isArray(removed); + assert.lengthOf(removed, 1); + assert.isObject(options); + done(); + }); + assert.equal(this.primitives.shift(), this.primitives); + }); + }); + + describe('eachOn()', () => { + it('should iterate over elements with extra meta', () => { + this.elements.eachOn((element, ix, elements, meta) => { + assert.hasAllKeys(meta, ['prev', 'next', 'isFirst', 'isLast']); + }); + }); + }); + + describe('mapOn()', () => { + it('should resolve a map, with iteration over elements with extra meta', () => { + assert.isArray(this.elements.mapOn((element, ix, elements, meta) => { + assert.hasAllKeys(meta, ['prev', 'next', 'isFirst', 'isLast']); + return element; + })); + }); + }); + + describe('reduceOn()', () => { + it('should resolve a memo, with iteration over elements with extra meta', () => { + assert.hasAllKeys(this.elements.reduceOn((memo, element, ix, elements, meta) => { + assert.hasAllKeys(meta, ['prev', 'next', 'isFirst', 'isLast']); + memo[element.message] = ix; + return memo; + }, {}), ['E1', 'E2', 'E3']); + }); + }); + + describe('containsBy()', () => { + it('should return true if contains the element', () => { + assert.isTrue(this.primitives.containsBy((element) => element === 3)); + }); + }); + + describe('pluckAll()', () => { + it('should return true if contains the element', () => { + const result = this.elements.pluckAll('message'); + assert.isArray(result); + assert.lengthOf(result, 3); + assert.sameDeepMembers(result, [{ message: 'E1' }, { message: 'E2' }, { message: 'E3' }]); + }); + }); + + describe('toJSON()', () => { + it('should serialize the collection into a json representation', () => { + const result = this.elements.toJSON(); + assert.isArray(result); + assert.lengthOf(result, 3); + assert.sameDeepMembers(result, this.mElements); + }); + }); + + describe('chain()', () => { + it('should chain', () => { + assert.isUndefined(this.elements.chain().filter((e) => e.value !== 3).find((e) => e.value === 3)); + assert.isFalse(this.elements.isChaining); + }); + }); + + /** Underscore Methods **/ + + describe('each()', () => { + it('should iterate over elements', () => { + this.elements.each((element) => assert.instanceOf(element, this.Generic)); + }); + }); + + describe('map()', () => { + it('should return an array of elements', () => { + const result = this.elements.map((element, ix, elements) => { + assert.instanceOf(element, this.Generic); + assert.isNumber(ix); + assert.isArray(elements); + return element; + }); + assert.isArray(result); + assert.lengthOf(result, 3); + }); + }); + + describe('reduce()', () => { + it('should resolve a memo by iterating over the elements', () => { + assert.hasAllKeys(this.elements.reduce((memo, element) => { + memo[element.message] = element.value; + return memo; + }, {}), ['E1', 'E2', 'E3']); + }); + }); + + describe('reduceRight()', () => { + it('should resolve a memo by iterating over the elements (from end to start)', () => { + assert.hasAllKeys(this.elements.reduceRight((memo, element) => { + memo[element.message] = element.value; + return memo; + }, {}), ['E1', 'E2', 'E3']); + }); + }); + + describe('find()', () => { + it('should find an element by predicate', () => { + assert.equal(this.primitives.find(element => element === 3), 3); + }); + }); + + describe('findIndex()', () => { + it('should find the index by predicate', () => { + assert.equal(this.elements.findIndex((element) => { + return element.value === 2; + }), 1); + }); + }); + + describe('filter()', () => { + it('should filter elements by predicate', () => { + assert.sameDeepMembers(this.elements.filter((element) => { + return element.value >= 2; + }), [this.mElements[1], this.mElements[2]]); + }); + }); + + describe('some()', () => { + it('should return true for some elements that pass predicate condition', () => { + assert.isTrue(this.elements.some((element) => element.value === 2)); + }); + }); + + describe('every()', () => { + it('should return true for all elements that pass predicate condition', () => { + assert.isTrue(this.elements.every((element) => [1, 2, 3].includes(element.value))); + }); + }); + + describe('reject()', () => { + it('should reject elements by predicate', () => { + const result = this.elements.reject((element) => element.value !== 2); + assert.sameDeepMembers(result, [this.mElements[1]]); + }); + }); + + describe('invoke()', () => { + it('should invoke a method over all the elements in the collection', () => { + const result = this.elements.invoke('say', 'Howdy!'); + assert.sameMembers(result, ['Howdy! E1', 'Howdy! E2', 'Howdy! E3']); + }); + }); + + describe('pluck()', () => { + it('should return a flat array with values for a given property', () => { + const result = this.elements.pluck('value'); + assert.sameMembers(result, [1, 2, 3]); + }); + }); + + describe('contains()', () => { + it('should return true, when the element is contained in the collection: primitives', () => { + assert.isTrue(this.primitives.contains(2)); + }); + it('should return true, when the element is contained in the collection: elements', () => { + assert.isTrue(this.elements.contains(this.elements.at(1))); + }); + }); + + describe('size()', () => { + it('should retrieve the size', () => { + assert.equal(this.elements.size(), 3); + }); + }); + + describe('first()', () => { + it('should retrieve the first element', () => { + assert.ownInclude(this.elements.first(), this.mElements[0]); + }); + }); + + describe('last()', () => { + it('should retrieve the last element', () => { + assert.ownInclude(this.elements.last(), this.mElements[2]); + }); + }); + + describe('max()', () => { + it('should retrieve the max value by predicate', () => { + assert.propertyVal(this.elements.max((element) => element.value > 2), 'message', 'E3'); + }); + }); + + describe('min()', () => { + it('should retrieve the min value by predicate', () => { + assert.propertyVal(this.elements.min((element) => element.value < 2), 'message', 'E2'); + }); + }); + + describe('groupBy()', () => { + it('should make groups by a predicate', () => { + const result = this.elements.groupBy((element) => { + return element.value === 1 ? 'G1' : 'G2'; + }); + assert.hasAllKeys(result, ['G1', 'G2']); + assert.ownInclude(result.G1, this.elements.at(0)); + assert.ownInclude(result.G2, this.elements.at(1)); + assert.ownInclude(result.G2, this.elements.at(2)); + }); + }); + + describe('countBy()', () => { + it('should count by a predicate', () => { + const result = this.elements.countBy((element) => { + return element.value <= 2 ? 'lessThan3' : 'greaterThan2'; + }); + assert.hasAllKeys(result, ['lessThan3', 'greaterThan2']); + assert.equal(result.lessThan3, 2); + assert.equal(result.greaterThan2, 1); + }); + }); + + describe('sample()', () => { + it('should retrieve a random sample from the collection', () => { + assert.instanceOf(this.elements.sample(), this.Generic); + }); + }); + + describe('partition()', () => { + it('should make different partitions by a predicate', () => { + const result = this.elements.partition((element) => element.value > 2); + + assert.isArray(result); + assert.include(result[0], this.elements.at(2)); + assert.include(result[1], this.elements.at(0)); + assert.include(result[1], this.elements.at(1)); + }); + }); + + describe('compact()', () => { + it('should compact values from the collection', () => { + this.primitives.push(false); + assert.notInclude(this.elements.compact(), false); + }); + }); + + describe('sortBy()', () => { + it('should sort elements', () => { + this.elements.sortBy((element) => element.value < 3); + assert.equal(this.elements.at(0).value, 3); + assert.equal(this.elements.at(1).value, 1); + assert.equal(this.elements.at(2).value, 2); + }); + }); + + describe('shuffle()', () => { + xit('should shuffle the elements', () => {}); + }); + }); +});