diff --git a/package.json b/package.json index 926da0e..499c74b 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,7 @@ "p-map": "^3.0.0" }, "devDependencies": { + "@babel/traverse": "^7.7.4", "@babel/types": "7.6.1", "@ember/optional-features": "^0.7.0", "@types/babel__traverse": "^7.0.7", diff --git a/src/checkup.ts b/src/checkup.ts index e873dc1..0715f61 100644 --- a/src/checkup.ts +++ b/src/checkup.ts @@ -1,7 +1,7 @@ import { IUserInterface, IProject, ITaskConstructor, IOptions, ITaskResult } from './interfaces'; import TaskList from './task-list'; import * as DefaultTasks from './tasks'; -import ResultConsoleWriter from './utils/result-console-writer'; +import ResultWriter from './utils/result-writer'; import Clock from './utils/clock'; const DEFAULT_TASKS = ( @@ -62,9 +62,9 @@ export default class Checkup { this.ui.stopProgress(); if (!this.options.silent) { - let writer = new ResultConsoleWriter(taskResults); + let writer = new ResultWriter(taskResults); - writer.write(); + writer.toConsole(); writer.writeDuration(clock.duration); } diff --git a/src/interfaces/index.ts b/src/interfaces/index.ts index 4ffb755..f2f3fd5 100644 --- a/src/interfaces/index.ts +++ b/src/interfaces/index.ts @@ -67,10 +67,6 @@ export interface IProject { root: string; } -export interface IResultConsoleWriter { - write: () => void; -} - export interface ISearchTraverser { hasResults: boolean; results: T; @@ -104,7 +100,8 @@ export interface ITaskList { } export interface ITaskResult { - write: (writer: IConsoleWriter) => void; + toConsole: (writer: IConsoleWriter) => void; + toJson: () => {}; } export interface ITestMetrics { diff --git a/src/results/dependencies-task-result.ts b/src/results/dependencies-task-result.ts index 276b4a0..92c75d1 100644 --- a/src/results/dependencies-task-result.ts +++ b/src/results/dependencies-task-result.ts @@ -9,8 +9,8 @@ export default class DependenciesTaskResult implements ITaskResult { this.emberLibraries = {}; } - write(writer: IConsoleWriter) { - writer.heading('Depedencies'); + toConsole(writer: IConsoleWriter) { + writer.heading('Dependencies'); writer.table('Ember Core Libraries', this.emberLibraries); writer.line(); @@ -18,4 +18,14 @@ export default class DependenciesTaskResult implements ITaskResult { writer.table('Ember CLI Addons', this.emberCliAddons.dependencies); writer.line(); } + + toJson() { + return { + dependencies: { + emberLibraries: this.emberLibraries, + emberAddons: this.emberAddons, + emberCliAddons: this.emberCliAddons, + }, + }; + } } diff --git a/src/results/project-info-task-result.ts b/src/results/project-info-task-result.ts index 5bf09e0..48479ed 100644 --- a/src/results/project-info-task-result.ts +++ b/src/results/project-info-task-result.ts @@ -5,7 +5,7 @@ export default class ProjectInfoTaskResult implements ITaskResult { name!: string; version!: string; - write(writer: IConsoleWriter) { + toConsole(writer: IConsoleWriter) { writer.heading('Project Information'); writer.column({ Name: this.name, @@ -14,4 +14,8 @@ export default class ProjectInfoTaskResult implements ITaskResult { }); writer.line(); } + + toJson() { + return { name: this.name, type: this.type, version: this.version }; + } } diff --git a/src/results/tests-task-result.ts b/src/results/tests-task-result.ts index 0c0e105..6160a30 100644 --- a/src/results/tests-task-result.ts +++ b/src/results/tests-task-result.ts @@ -61,8 +61,12 @@ export default class TestsTaskResult implements ITaskResult { } } - write(writer: IConsoleWriter) { + toConsole(writer: IConsoleWriter) { writer.heading('Implement Me!'); writer.line(); } + + toJson() { + return { tests: this.basic }; + } } diff --git a/src/results/types-task-result.ts b/src/results/types-task-result.ts index f6f9131..c26eeec 100644 --- a/src/results/types-task-result.ts +++ b/src/results/types-task-result.ts @@ -4,9 +4,13 @@ import getTaskItemTotals from '../utils/get-task-item-totals'; export default class TypesTaskResult implements ITaskResult { types!: ITaskItemData; - write(writer: IConsoleWriter) { + toConsole(writer: IConsoleWriter) { writer.heading('Types'); writer.table(['Type', 'Total Count'], getTaskItemTotals(this.types)); writer.line(); } + + toJson() { + return { types: this.types }; + } } diff --git a/src/tests/unit/checkup-test.ts b/src/tests/unit/checkup-test.ts index 0b3b8e1..cd63094 100644 --- a/src/tests/unit/checkup-test.ts +++ b/src/tests/unit/checkup-test.ts @@ -14,9 +14,13 @@ class FakeTaskResult implements ITaskResult { name!: string; version!: string; - write(writer: IConsoleWriter) { + toConsole(writer: IConsoleWriter) { writer.line(); } + + toJson() { + return { name: this.name, version: this.version }; + } } class FakeTask extends Task implements ITask { diff --git a/src/tests/unit/tests-task-test.ts b/src/tests/unit/tests-task-test.ts index d5341be..a488ca5 100644 --- a/src/tests/unit/tests-task-test.ts +++ b/src/tests/unit/tests-task-test.ts @@ -379,6 +379,18 @@ module('tests-task', function(hooks) { assert.equal(unitData.moduleCount, 2, 'unit module count is correct'); assert.equal(unitData.skipCount, 1, 'unit skip count is correct'); assert.equal(unitData.testCount, 2, 'unit test count is correct'); + + // @TODO: Make this as a separate test once the test fixtures are in defined in a separate file + const expectedJsonResult = { + tests: { + application: { moduleCount: 0, skipCount: 0, testCount: 0 }, + container: { moduleCount: 0, skipCount: 0, testCount: 0 }, + rendering: { moduleCount: 0, skipCount: 0, testCount: 0 }, + unit: { moduleCount: 2, skipCount: 1, testCount: 2 }, + }, + }; + + assert.deepEqual(result.toJson(), expectedJsonResult, 'toJson output is correct'); }); test('it finds container tests', async function(assert) { @@ -746,6 +758,18 @@ module('tests-task', function(hooks) { assert.equal(unitData.moduleCount, 1, 'unit module count is correct'); assert.equal(unitData.skipCount, 1, 'unit skip count is correct'); assert.equal(unitData.testCount, 2, 'unit test count is correct'); + + // @TODO: Make this as a separate test once the test fixtures are in defined in a separate file + const expectedJsonResult = { + tests: { + application: { moduleCount: 2, skipCount: 1, testCount: 2 }, + container: { moduleCount: 1, skipCount: 0, testCount: 2 }, + rendering: { moduleCount: 1, skipCount: 0, testCount: 1 }, + unit: { moduleCount: 1, skipCount: 1, testCount: 2 }, + }, + }; + + assert.deepEqual(result.toJson(), expectedJsonResult, 'toJson output is correct'); }); }); }); diff --git a/src/tests/unit/types-task-test.ts b/src/tests/unit/types-task-test.ts index dddb9cc..4c029cb 100644 --- a/src/tests/unit/types-task-test.ts +++ b/src/tests/unit/types-task-test.ts @@ -109,5 +109,44 @@ module('types-task', function(hooks) { assert.equal(typesTaskResult.types.routes.length, 2); assert.equal(typesTaskResult.types.services.length, 2); assert.equal(typesTaskResult.types.templates.length, 2); + + // @TODO: Make this as a separate test once the test fixtures are in defined in a separate file + const expectedJsonResult = { + types: { + components: [ + 'addon/components/my-component.js', + 'lib/ember-super-button/addon/components/my-component.js', + ], + controllers: [ + 'addon/controllers/my-controller.js', + 'lib/ember-super-button/addon/controllers/my-controller.js', + ], + helpers: [ + 'addon/helpers/my-helper.js', + 'lib/ember-super-button/addon/helpers/my-helper.js', + ], + initializers: [ + 'addon/initializers/my-initializer.js', + 'lib/ember-super-button/addon/initializers/my-initializer.js', + ], + 'instance-initializers': [ + 'addon/instance-initializers/my-helper.js', + 'lib/ember-super-button/addon/instance-initializers/my-helper.js', + ], + mixins: ['addon/mixins/my-mixin.js', 'lib/ember-super-button/addon/mixins/my-mixin.js'], + models: ['addon/models/my-model.js', 'lib/ember-super-button/addon/models/my-model.js'], + routes: ['addon/routes/my-route.js', 'lib/ember-super-button/addon/routes/my-route.js'], + services: [ + 'addon/services/my-service.js', + 'lib/ember-super-button/addon/services/my-service.js', + ], + templates: [ + 'addon/templates/my-component.hbs', + 'lib/ember-super-button/addon/templates/my-component.hbs', + ], + }, + }; + + assert.deepEqual(typesTaskResult.toJson(), expectedJsonResult, 'toJson output is correct'); }); }); diff --git a/src/tests/unit/utils/mock-console.ts b/src/tests/unit/utils/mock-console.ts new file mode 100644 index 0000000..2102948 --- /dev/null +++ b/src/tests/unit/utils/mock-console.ts @@ -0,0 +1,15 @@ +export default class MockConsole { + _buffer: string[]; + log: (message: string) => void; + + constructor() { + this._buffer = []; + this.log = message => { + this._buffer.push(message); + }; + } + + toString() { + return this._buffer.join('\n'); + } +} diff --git a/src/tests/unit/utils/result-writer-test.ts b/src/tests/unit/utils/result-writer-test.ts new file mode 100644 index 0000000..0d70963 --- /dev/null +++ b/src/tests/unit/utils/result-writer-test.ts @@ -0,0 +1,203 @@ +// @ts-ignore +import IFixturifyProject = require('ember-cli/tests/helpers/fixturify-project'); +const DisposableFixturifyProject = require('../../helpers/DisposableFixturifyProject'); +const MockUI = require('console-ui/mock'); + +import { + IProject, + ITaskResult, + IConsoleWriter, + ITask, + ITaskConstructor, +} from '../../../interfaces'; +import Checkup from '../../../checkup'; +import ResultWriter from '../../../utils/result-writer'; +import MockConsole from '../utils/mock-console'; +import ConsoleWriter from '../../../utils/console-writer'; +import Task from '../../..//task'; + +const { test } = QUnit; + +const FILE_PATH = 'tests/testApp'; + +const TYPES = { + components: { + 'my-component.js': '', + }, + controllers: { + 'my-controller.js': '', + }, + helpers: { + 'my-helper.js': '', + }, + initializers: { + 'my-initializer.js': '', + }, + 'instance-initializers': { + 'my-helper.js': '', + }, + mixins: { + 'my-mixin.js': '', + }, + models: { + 'my-model.js': '', + }, + routes: { + 'my-route.js': '', + }, + services: { + 'my-service.js': '', + }, + templates: { + 'my-component.hbs': '', + }, +}; + +const TESTS = { + unit: { + utils: { + 'foo-test.js': ` + import { module, test } from 'qunit'; + + module('Unit | Utils | foo', function() { + test('bar', function(assert) { + assert.ok(true); + }); + + skip('biz', function(assert) { + assert.ok(true); + }); + + test('barbiz', function(assert) { + assert.ok(true); + }); + }); + `, + }, + }, +}; + +class FakeTaskResult implements ITaskResult { + name!: string; + version!: string; + + toConsole(writer: IConsoleWriter) { + writer.text(`name: ${this.name}, version: ${this.version}`); + } + + toJson() { + return { name: this.name, version: this.version }; + } +} + +class FakeTask extends Task implements ITask { + constructor(project: IProject) { + super(project); + } + + async run(): Promise { + let result = new FakeTaskResult(); + result.name = 'Foobar'; + result.version = 'latest'; + + return result; + } +} + +QUnit.module('result-writer', function(hooks) { + let fixturifyProject: IFixturifyProject; + let project: IProject; + + hooks.beforeEach(function() { + fixturifyProject = new DisposableFixturifyProject('cli-checkup-app', '0.0.0'); + fixturifyProject.addDevDependency('ember-cli-string-utils', 'latest'); + fixturifyProject.addDevDependency('ember-source', 'latest'); + fixturifyProject.addDevDependency('ember-cli', 'latest'); + fixturifyProject.addDevDependency('ember-data', 'latest'); + fixturifyProject.addAddon('ember-cli-blueprint-test-helpers', 'latest'); + project = fixturifyProject.buildProjectModel(); + }); + + hooks.afterEach(function() { + fixturifyProject.dispose(FILE_PATH); + }); + + test('result writer outputs json output correctly for the default tasks', async function(assert) { + fixturifyProject.files = Object.assign(fixturifyProject.files, { + 'index.js': 'index js file', + addon: TYPES, + tests: TESTS, + }); + + project = fixturifyProject.buildProjectModel(); + let checkup = new Checkup({ silent: true }, project, new MockUI()); + let result: ITaskResult[] = await checkup.run(); + let writer = new ResultWriter(result); + + const expectedJsonResult = { + tasks: [ + { + types: { + components: ['addon/components/my-component.js'], + controllers: ['addon/controllers/my-controller.js'], + helpers: ['addon/helpers/my-helper.js'], + initializers: ['addon/initializers/my-initializer.js'], + 'instance-initializers': ['addon/instance-initializers/my-helper.js'], + mixins: ['addon/mixins/my-mixin.js'], + models: ['addon/models/my-model.js'], + routes: ['addon/routes/my-route.js'], + services: ['addon/services/my-service.js'], + templates: ['addon/templates/my-component.hbs'], + }, + }, + { + tests: { + application: { moduleCount: 0, skipCount: 0, testCount: 0 }, + container: { moduleCount: 0, skipCount: 0, testCount: 0 }, + rendering: { moduleCount: 0, skipCount: 0, testCount: 0 }, + unit: { moduleCount: 1, skipCount: 1, testCount: 2 }, + }, + }, + ], + dependencies: { + emberLibraries: { 'ember-source': 'latest', 'ember-cli': 'latest', 'ember-data': 'latest' }, + emberAddons: { + dependencies: {}, + devDependencies: { 'ember-source': 'latest', 'ember-data': 'latest' }, + }, + emberCliAddons: { + dependencies: { 'ember-cli-blueprint-test-helpers': 'latest' }, + devDependencies: { 'ember-cli-string-utils': 'latest', 'ember-cli': 'latest' }, + }, + }, + name: 'cli-checkup-app', + type: 'application', + version: '0.0.0', + }; + + assert.deepEqual( + writer.toJson(), + expectedJsonResult, + 'toJson for all the tasks works correctly' + ); + }); + + test('result writer outputs console output correctly for all tasks', async function(assert) { + let tasks: ITaskConstructor[] = [FakeTask]; + let checkup = new Checkup({ silent: true }, project, new MockUI(), tasks); + + // Console specific setup + let mockConsole = new MockConsole(); + let consoleWriter = new ConsoleWriter(mockConsole.log); + + let result: ITaskResult[] = await checkup.run(); + let writer = new ResultWriter(result, consoleWriter); + + writer.toConsole(); + assert.equal( + mockConsole.toString(), + ` \nname: Foobar, version: latest`, + 'toConsole works correctly' + ); + }); +}); diff --git a/src/utils/result-console-writer.ts b/src/utils/result-console-writer.ts deleted file mode 100644 index d75d0d8..0000000 --- a/src/utils/result-console-writer.ts +++ /dev/null @@ -1,25 +0,0 @@ -import ConsoleWriter from './console-writer'; -import { IResultConsoleWriter, ITaskResult, IConsoleWriter } from '../interfaces'; - -export default class ResultConsoleWriter implements IResultConsoleWriter { - results: ITaskResult[]; - writer: IConsoleWriter; - - constructor(results: ITaskResult[]) { - this.results = results; - this.writer = new ConsoleWriter(); - } - - write() { - this.writer.line(); - - this.results.forEach(result => { - result.write(this.writer); - }); - } - - writeDuration(duration: string) { - this.writer.text(`✨ Checkup complete in ${duration}s.`); - this.writer.line(); - } -} diff --git a/src/utils/result-writer.ts b/src/utils/result-writer.ts new file mode 100644 index 0000000..349acb7 --- /dev/null +++ b/src/utils/result-writer.ts @@ -0,0 +1,63 @@ +import ConsoleWriter from './console-writer'; +import { ITaskResult, IConsoleWriter } from '../interfaces'; + +export default class ResultWriter { + results: ITaskResult[]; + writer: IConsoleWriter; + + constructor(results: ITaskResult[], writer: IConsoleWriter = new ConsoleWriter()) { + this.results = results; + this.writer = writer; + } + + toConsole() { + this.writer.line(); + + this.results.forEach(result => { + result.toConsole(this.writer); + }); + } + + /** + * Returns a json object with the project metadata and info of all the tasks that were run. + * Example output for a project named foo and its 4 tasks: + * { + * name: 'foo', + * version: 'bar', + * type: 'baz', + * tasks: [ + * types: {}, + * tests: {}, + * angleBracket: {}, + * nativeClass: {} + * ], + * dependencies: {} + * } + * + */ + toJson() { + const resultData = { + tasks: [] as object[], + }; + + this.results.forEach(taskResult => { + if ( + ['ProjectInfoTaskResult', 'DependenciesTaskResult'].includes(taskResult.constructor.name) + ) { + // Adds metadata and dependencies related information + // to the resultData object + Object.assign(resultData, taskResult.toJson()); + } else { + // Adds all other tasks in the `tasks` array if the resultData object + resultData.tasks.push(taskResult.toJson()); + } + }); + + return resultData; + } + + writeDuration(duration: string) { + this.writer.text(`✨ Checkup complete in ${duration}s.`); + this.writer.line(); + } +}