From 81db470a5a33dc85b25d9d26f130fda79fab25ed Mon Sep 17 00:00:00 2001 From: Manuel Friedli Date: Thu, 6 Sep 2018 14:37:20 +0200 Subject: [PATCH 01/10] Refactor input fields, converter selectors and error messages into their own components. --- src/app/app.component.html | 17 +-- src/app/app.component.scss | 103 ------------------ src/app/app.component.ts | 40 +------ src/app/app.module.ts | 8 +- .../converter-selector.component.html | 9 ++ .../converter-selector.component.scss | 74 +++++++++++++ .../converter-selector.component.spec.ts | 25 +++++ .../converter-selector.component.ts | 30 +++++ .../error-message.component.html | 1 + .../error-message.component.scss | 4 + .../error-message.component.spec.ts | 25 +++++ .../error-message/error-message.component.ts | 15 +++ .../text-input-field.component.html | 4 + .../text-input-field.component.scss | 69 ++++++++++++ .../text-input-field.component.spec.ts | 25 +++++ .../text-input-field.component.ts | 45 ++++++++ 16 files changed, 337 insertions(+), 157 deletions(-) create mode 100644 src/app/converter-selector/converter-selector.component.html create mode 100644 src/app/converter-selector/converter-selector.component.scss create mode 100644 src/app/converter-selector/converter-selector.component.spec.ts create mode 100644 src/app/converter-selector/converter-selector.component.ts create mode 100644 src/app/error-message/error-message.component.html create mode 100644 src/app/error-message/error-message.component.scss create mode 100644 src/app/error-message/error-message.component.spec.ts create mode 100644 src/app/error-message/error-message.component.ts create mode 100644 src/app/text-input-field/text-input-field.component.html create mode 100644 src/app/text-input-field/text-input-field.component.scss create mode 100644 src/app/text-input-field/text-input-field.component.spec.ts create mode 100644 src/app/text-input-field/text-input-field.component.ts diff --git a/src/app/app.component.html b/src/app/app.component.html index c96ed95..35fc20f 100644 --- a/src/app/app.component.html +++ b/src/app/app.component.html @@ -1,18 +1,7 @@
-
- -
-
-
- -
-
-
+ + +
diff --git a/src/app/app.component.scss b/src/app/app.component.scss index 9a81a0c..63be152 100644 --- a/src/app/app.component.scss +++ b/src/app/app.component.scss @@ -2,106 +2,3 @@ font-family: "ABeeZee", sans-serif; margin: 0 1em 1em 1em; } - -.textwrapper { - margin: 0 0 1em 0; - padding: 0 1em 0 0; -} - -.arrow_box { - position: relative; - background: #fff; - border: 1px solid #aaa; - &:focus { - border-color: #888; - } - &:hover { - border-color: #333; - } - &:after, &:before { - top: 100%; - left: 50%; - border: solid transparent; - content: " "; - height: 0; - width: 0; - position: absolute; - pointer-events: none; - } - &:after { - border-color: rgba(255, 255, 255, 0); - border-top-color: #fff; - border-width: 1em; - margin-left: -1em; - } - &:before { - border-color: rgba(170, 170, 170, 0); - border-top-color: #aaa; - border-width: calc(1em + 1px); - margin-left: calc(-1em - 1px); - } - &:focus:before { - border-color: rgba(136, 136, 136, 0); - border-top-color: #888; - } - &:hover:before { - border-color: rgba(51, 51, 51, 0); - border-top-color: #333; - } - .selectwrapper > & { - display: inline-block; - } -} - -.textinput { - background-color: #fff; - border: none; - color: #000; - font-family: "Free Monospaced", monospace; - height: 10em; - margin: 0; - padding: 0.5em; - resize: vertical; - width: 100%; - &:focus { - border-color: #888; - } - &:hover { - border-color: #333; - } -} - -.selectwrapper { - margin: 0 0 1em 0; - padding: 0; - text-align: center; - &.error { - > .arrow_box { - border-color: red; - &:before { - border-top-color: red; - } - } - select { - color: red; - } - } -} - -.select { - background-color: #fff; - border: none; - color: #000; - font-family: "ABeeZee", sans-serif; - margin: 0; - padding: 0.5em; -} - -.option { - /* font-family: "ABeeZee", sans-serif;*/ -} - -.errormessage { - color: red; - text-align: center; -} diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 820811b..051970e 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -1,8 +1,6 @@ import {Component, OnInit} from '@angular/core'; -import {ConverterRegistryService} from './converter-registry.service'; import {InputComponentManagerService} from './input-component-manager.service'; import {Step} from './step'; -import {Converter} from './converter/converter'; @Component({ selector: 'app-root', @@ -11,47 +9,11 @@ import {Converter} from './converter/converter'; }) export class AppComponent implements OnInit { public steps: Step[] = []; - public converters: Converter[] = []; - constructor(private converterRegistryService: ConverterRegistryService, - private inputComponentManagerService: InputComponentManagerService) { - } - - convert(step: Step, $event: any): void { - step.selectedConverter = this.converterRegistryService.getConverter($event.target.selectedOptions[0].id); - this.update(step); - } - - update(step: Step): void { - const converter: Converter = step.selectedConverter; - - if (converter !== undefined) { - const content: string = step.content; - let result: string; - try { - result = converter.convert(content); - } catch (error) { - if (typeof console === 'object' && typeof console.log === 'function') { - console.log(error); - } - step.message = error.message; - step.error = true; - result = null; - } - if (result !== null) { - step.message = ''; - step.error = false; - if (result !== '') { - const nextComponent: Step = this.inputComponentManagerService.getNext(step); - nextComponent.content = result; - this.update(nextComponent); - } - } - } + constructor(private inputComponentManagerService: InputComponentManagerService) { } ngOnInit(): void { - this.converters = this.converterRegistryService.getAllConverters(); this.steps = this.inputComponentManagerService.getAllComponents(); this.inputComponentManagerService.getFirst(); } diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 5e40ab8..875f69f 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -6,11 +6,17 @@ import {InputComponentManagerService} from './input-component-manager.service'; import {NativeLibraryWrapperService} from './native-library-wrapper.service'; import {FormsModule} from '@angular/forms'; import {VersionComponent} from './version/version.component'; +import { TextInputFieldComponent } from './text-input-field/text-input-field.component'; +import { ConverterSelectorComponent } from './converter-selector/converter-selector.component'; +import { ErrorMessageComponent } from './error-message/error-message.component'; @NgModule({ declarations: [ AppComponent, - VersionComponent + VersionComponent, + TextInputFieldComponent, + ConverterSelectorComponent, + ErrorMessageComponent ], imports: [ BrowserModule, diff --git a/src/app/converter-selector/converter-selector.component.html b/src/app/converter-selector/converter-selector.component.html new file mode 100644 index 0000000..b15dd1b --- /dev/null +++ b/src/app/converter-selector/converter-selector.component.html @@ -0,0 +1,9 @@ +
+
+ +
+
diff --git a/src/app/converter-selector/converter-selector.component.scss b/src/app/converter-selector/converter-selector.component.scss new file mode 100644 index 0000000..b31f217 --- /dev/null +++ b/src/app/converter-selector/converter-selector.component.scss @@ -0,0 +1,74 @@ +.selectwrapper { + margin: 0 0 1em 0; + padding: 0; + text-align: center; + &.error { + > .arrow_box { + border-color: red; + &:before { + border-top-color: red; + } + } + select { + color: red; + } + } +} + +.select { + background-color: #fff; + border: none; + color: #000; + font-family: "ABeeZee", sans-serif; + margin: 0; + padding: 0.5em; +} + +.arrow_box { + position: relative; + background: #fff; + border: 1px solid #aaa; + &:focus { + border-color: #888; + } + &:hover { + border-color: #333; + } + &:after, &:before { + top: 100%; + left: 50%; + border: solid transparent; + content: " "; + height: 0; + width: 0; + position: absolute; + pointer-events: none; + } + &:after { + border-color: rgba(255, 255, 255, 0); + border-top-color: #fff; + border-width: 1em; + margin-left: -1em; + } + &:before { + border-color: rgba(170, 170, 170, 0); + border-top-color: #aaa; + border-width: calc(1em + 1px); + margin-left: calc(-1em - 1px); + } + &:focus:before { + border-color: rgba(136, 136, 136, 0); + border-top-color: #888; + } + &:hover:before { + border-color: rgba(51, 51, 51, 0); + border-top-color: #333; + } + .selectwrapper > & { + display: inline-block; + } +} + +.option { + /* font-family: "ABeeZee", sans-serif;*/ +} diff --git a/src/app/converter-selector/converter-selector.component.spec.ts b/src/app/converter-selector/converter-selector.component.spec.ts new file mode 100644 index 0000000..410aaa7 --- /dev/null +++ b/src/app/converter-selector/converter-selector.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ConverterSelectorComponent } from './converter-selector.component'; + +describe('ConverterSelectorComponent', () => { + let component: ConverterSelectorComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ ConverterSelectorComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(ConverterSelectorComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/converter-selector/converter-selector.component.ts b/src/app/converter-selector/converter-selector.component.ts new file mode 100644 index 0000000..c3b6230 --- /dev/null +++ b/src/app/converter-selector/converter-selector.component.ts @@ -0,0 +1,30 @@ +import {Component, Input, OnInit} from '@angular/core'; +import {Step} from '../step'; +import {Converter} from '../converter/converter'; +import {ConverterRegistryService} from '../converter-registry.service'; +import {TextInputFieldComponent} from '../text-input-field/text-input-field.component'; + +@Component({ + selector: 'app-converter-selector', + templateUrl: './converter-selector.component.html', + styleUrls: ['./converter-selector.component.scss'] +}) +export class ConverterSelectorComponent implements OnInit { + @Input() + public step: Step; + @Input() + private textInput: TextInputFieldComponent; + public converters: Converter[] = []; + + constructor(private converterRegistryService: ConverterRegistryService) { + } + + convert(step: Step, $event: any): void { + step.selectedConverter = this.converterRegistryService.getConverter($event.target.selectedOptions[0].id); + this.textInput.update(step); + } + + ngOnInit() { + this.converters = this.converterRegistryService.getAllConverters(); + } +} diff --git a/src/app/error-message/error-message.component.html b/src/app/error-message/error-message.component.html new file mode 100644 index 0000000..6c306bd --- /dev/null +++ b/src/app/error-message/error-message.component.html @@ -0,0 +1 @@ +
diff --git a/src/app/error-message/error-message.component.scss b/src/app/error-message/error-message.component.scss new file mode 100644 index 0000000..ef9b713 --- /dev/null +++ b/src/app/error-message/error-message.component.scss @@ -0,0 +1,4 @@ +.errormessage { + color: red; + text-align: center; +} diff --git a/src/app/error-message/error-message.component.spec.ts b/src/app/error-message/error-message.component.spec.ts new file mode 100644 index 0000000..d90c1d5 --- /dev/null +++ b/src/app/error-message/error-message.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ErrorMessageComponent } from './error-message.component'; + +describe('ErrorMessageComponent', () => { + let component: ErrorMessageComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ ErrorMessageComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(ErrorMessageComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/error-message/error-message.component.ts b/src/app/error-message/error-message.component.ts new file mode 100644 index 0000000..8db15df --- /dev/null +++ b/src/app/error-message/error-message.component.ts @@ -0,0 +1,15 @@ +import {Component, Input} from '@angular/core'; +import {Step} from '../step'; + +@Component({ + selector: 'app-error-message', + templateUrl: './error-message.component.html', + styleUrls: ['./error-message.component.scss'] +}) +export class ErrorMessageComponent { + @Input() + public step: Step; + + constructor() { + } +} diff --git a/src/app/text-input-field/text-input-field.component.html b/src/app/text-input-field/text-input-field.component.html new file mode 100644 index 0000000..7000fb7 --- /dev/null +++ b/src/app/text-input-field/text-input-field.component.html @@ -0,0 +1,4 @@ +
+ +
diff --git a/src/app/text-input-field/text-input-field.component.scss b/src/app/text-input-field/text-input-field.component.scss new file mode 100644 index 0000000..89dc5f9 --- /dev/null +++ b/src/app/text-input-field/text-input-field.component.scss @@ -0,0 +1,69 @@ +.textwrapper { + margin: 0 0 1em 0; + padding: 0 1em 0 0; +} + +.arrow_box { + position: relative; + background: #fff; + border: 1px solid #aaa; + &:focus { + border-color: #888; + } + &:hover { + border-color: #333; + } + &:after, &:before { + top: 100%; + left: 50%; + border: solid transparent; + content: " "; + height: 0; + width: 0; + position: absolute; + pointer-events: none; + } + &:after { + border-color: rgba(255, 255, 255, 0); + border-top-color: #fff; + border-width: 1em; + margin-left: -1em; + } + &:before { + border-color: rgba(170, 170, 170, 0); + border-top-color: #aaa; + border-width: calc(1em + 1px); + margin-left: calc(-1em - 1px); + } + &:focus:before { + border-color: rgba(136, 136, 136, 0); + border-top-color: #888; + } + &:hover:before { + border-color: rgba(51, 51, 51, 0); + border-top-color: #333; + } +} + +.textinput { + background-color: #fff; + border: none; + color: #000; + font-family: "Free Monospaced", monospace; + height: 10em; + margin: 0; + padding: 0.5em; + resize: vertical; + width: 100%; + &:focus { + border-color: #888; + } + &:hover { + border-color: #333; + } +} + +.errormessage { + color: red; + text-align: center; +} diff --git a/src/app/text-input-field/text-input-field.component.spec.ts b/src/app/text-input-field/text-input-field.component.spec.ts new file mode 100644 index 0000000..144588c --- /dev/null +++ b/src/app/text-input-field/text-input-field.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { TextInputFieldComponent } from './text-input-field.component'; + +describe('TextInputFieldComponent', () => { + let component: TextInputFieldComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ TextInputFieldComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(TextInputFieldComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/text-input-field/text-input-field.component.ts b/src/app/text-input-field/text-input-field.component.ts new file mode 100644 index 0000000..932db29 --- /dev/null +++ b/src/app/text-input-field/text-input-field.component.ts @@ -0,0 +1,45 @@ +import {Component, Input} from '@angular/core'; +import {Step} from '../step'; +import {Converter} from '../converter/converter'; +import {InputComponentManagerService} from '../input-component-manager.service'; + +@Component({ + selector: 'app-text-input-field', + templateUrl: './text-input-field.component.html', + styleUrls: ['./text-input-field.component.scss'] +}) +export class TextInputFieldComponent { + @Input() + public step: Step; + + constructor(private inputComponentManagerService: InputComponentManagerService) { + } + + update(step: Step): void { + const converter: Converter = step.selectedConverter; + + if (converter !== undefined) { + const content: string = step.content; + let result: string; + try { + result = converter.convert(content); + } catch (error) { + if (typeof console === 'object' && typeof console.log === 'function') { + console.log(error); + } + step.message = error.message; + step.error = true; + result = null; + } + if (result !== null) { + step.message = ''; + step.error = false; + if (result !== '') { + const nextComponent: Step = this.inputComponentManagerService.getNext(step); + nextComponent.content = result; + this.update(nextComponent); + } + } + } + } +} From 635b84fbd7da34c3101edef8abccbb9d237089bf Mon Sep 17 00:00:00 2001 From: Manuel Friedli Date: Thu, 6 Sep 2018 14:45:17 +0200 Subject: [PATCH 02/10] Don't pass step variable, it is not necessary --- .../converter-selector/converter-selector.component.html | 2 +- src/app/converter-selector/converter-selector.component.ts | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/app/converter-selector/converter-selector.component.html b/src/app/converter-selector/converter-selector.component.html index b15dd1b..5c3364c 100644 --- a/src/app/converter-selector/converter-selector.component.html +++ b/src/app/converter-selector/converter-selector.component.html @@ -1,6 +1,6 @@
- diff --git a/src/app/converter-selector/converter-selector.component.ts b/src/app/converter-selector/converter-selector.component.ts index c3b6230..5aeba29 100644 --- a/src/app/converter-selector/converter-selector.component.ts +++ b/src/app/converter-selector/converter-selector.component.ts @@ -19,9 +19,9 @@ export class ConverterSelectorComponent implements OnInit { constructor(private converterRegistryService: ConverterRegistryService) { } - convert(step: Step, $event: any): void { - step.selectedConverter = this.converterRegistryService.getConverter($event.target.selectedOptions[0].id); - this.textInput.update(step); + convert($event: any): void { + this.step.selectedConverter = this.converterRegistryService.getConverter($event.target.selectedOptions[0].id); + this.textInput.update(this.step); } ngOnInit() { From 86574e861651186af75656717c5ddf5bc4e74f8d Mon Sep 17 00:00:00 2001 From: Manuel Friedli Date: Thu, 6 Sep 2018 14:45:21 +0200 Subject: [PATCH 03/10] Make code shorter. --- src/app/converter-registry.service.ts | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/src/app/converter-registry.service.ts b/src/app/converter-registry.service.ts index d84733b..85e6304 100644 --- a/src/app/converter-registry.service.ts +++ b/src/app/converter-registry.service.ts @@ -36,12 +36,7 @@ export class ConverterRegistryService { } public getConverter(id: string): Converter { - for (let i = 0; i < this.converters.length; i++) { - if (this.converters[i].getId() === id) { - return this.converters[i]; - } - } - return undefined; + return this.converters.find((converter: Converter): boolean => converter.getId() === id); } private init(): void { @@ -67,11 +62,11 @@ export class ConverterRegistryService { } private registerConverter(converter: Converter): void { - this.converters.forEach((c: Converter) => { - if (c.getId() === converter.getId()) { - throw new Error('Converter-ID ' + converter.getId() + ' is already registered!'); - } - }); + // Don't allow duplicate registration of the same converter id + if (this.converters.some((c: Converter): boolean => c.getId() === converter.getId())) { + throw new Error(`Converter-ID ${converter.getId()} is already registered!`); + } + this.converters.push(converter); } } From e6b38fabcbb5b10a0b74a76f56db072447f0b9ac Mon Sep 17 00:00:00 2001 From: Manuel Friedli Date: Fri, 7 Sep 2018 11:24:24 +0200 Subject: [PATCH 04/10] Fix AppComponent test --- src/app/app.component.spec.ts | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/app/app.component.spec.ts b/src/app/app.component.spec.ts index 36d8222..18d20b6 100644 --- a/src/app/app.component.spec.ts +++ b/src/app/app.component.spec.ts @@ -3,8 +3,10 @@ import {async, ComponentFixture, TestBed} from '@angular/core/testing'; import {FormsModule} from '@angular/forms'; import {InputComponentManagerService} from './input-component-manager.service'; import {Step} from './step'; -import {ConverterRegistryService} from './converter-registry.service'; import {VersionComponent} from './version/version.component'; +import {ConverterSelectorComponent} from './converter-selector/converter-selector.component'; +import {TextInputFieldComponent} from './text-input-field/text-input-field.component'; +import {ErrorMessageComponent} from './error-message/error-message.component'; import createSpyObj = jasmine.createSpyObj; describe('AppComponent', () => { @@ -21,11 +23,16 @@ describe('AppComponent', () => { beforeEach(async(() => { TestBed.configureTestingModule({ - declarations: [AppComponent, VersionComponent], + declarations: [ + AppComponent, + ConverterSelectorComponent, + ErrorMessageComponent, + TextInputFieldComponent, + VersionComponent + ], imports: [FormsModule], providers: [ - {provide: InputComponentManagerService, useValue: inputComponentManagerServiceStub}, - {provide: ConverterRegistryService, useValue: converterRegistryServiceStub} + {provide: InputComponentManagerService, useValue: inputComponentManagerServiceStub} ] }) .compileComponents(); From 4bfce46f70e74d52a958be601ffb7fa0c6b6c411 Mon Sep 17 00:00:00 2001 From: Manuel Friedli Date: Fri, 7 Sep 2018 11:25:08 +0200 Subject: [PATCH 05/10] Implement TextInputFieldComponent spec --- .../text-input-field.component.spec.ts | 105 ++++++++++++++++-- 1 file changed, 95 insertions(+), 10 deletions(-) diff --git a/src/app/text-input-field/text-input-field.component.spec.ts b/src/app/text-input-field/text-input-field.component.spec.ts index 144588c..d71f04a 100644 --- a/src/app/text-input-field/text-input-field.component.spec.ts +++ b/src/app/text-input-field/text-input-field.component.spec.ts @@ -1,25 +1,110 @@ -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import {async, ComponentFixture, TestBed} from '@angular/core/testing'; -import { TextInputFieldComponent } from './text-input-field.component'; +import {TextInputFieldComponent} from './text-input-field.component'; +import {Step} from '../step'; +import {Component, ViewChild} from '@angular/core'; +import {FormsModule} from '@angular/forms'; +import {InputComponentManagerService} from '../input-component-manager.service'; +import createSpyObj = jasmine.createSpyObj; + +@Component({ + template: '' +}) +class TestHostComponent { + public step: Step = new Step(42); + @ViewChild(TextInputFieldComponent) + public sutComponent: TextInputFieldComponent; +} describe('TextInputFieldComponent', () => { - let component: TextInputFieldComponent; - let fixture: ComponentFixture; + let testHostComponent: TestHostComponent; + let testHostFixture: ComponentFixture; + + const inputComponentManagerServiceStub = createSpyObj(['getNext']); + inputComponentManagerServiceStub.getNext.and.returnValue(undefined); beforeEach(async(() => { TestBed.configureTestingModule({ - declarations: [ TextInputFieldComponent ] + declarations: [ + TestHostComponent, + TextInputFieldComponent + ], + imports: [FormsModule], + providers: [{provide: InputComponentManagerService, useValue: inputComponentManagerServiceStub}] }) - .compileComponents(); + .compileComponents(); })); beforeEach(() => { - fixture = TestBed.createComponent(TextInputFieldComponent); - component = fixture.componentInstance; - fixture.detectChanges(); + testHostFixture = TestBed.createComponent(TestHostComponent); + testHostComponent = testHostFixture.componentInstance; + testHostFixture.detectChanges(); }); it('should create', () => { - expect(component).toBeTruthy(); + expect(testHostComponent.sutComponent).toBeTruthy(); + }); + + it('should not perform an update if no converter is selected', () => { + // arrange + const step: Step = testHostComponent.step; + step.content = 'Don\'t you change me!'; + step.error = false; + step.message = 'There be no message.'; + + // act + testHostComponent.sutComponent.update(step); + + // assert + expect(step.content).toEqual('Don\'t you change me!'); + expect(step.error).toBeFalsy(); + expect(step.message).toEqual('There be no message.'); + }); + + it('should set the error flag if the conversion fails', () => { + // arrange + const step: Step = testHostComponent.step; + step.content = 'Don\'t you change me!'; + step.error = false; + step.message = 'There be no message.'; + step.selectedConverter = { + getId: () => 'testConverter', + convert: () => { + throw new Error('Test error'); + }, + getDisplayname: () => 'Testing converter' + }; + + // act + testHostComponent.sutComponent.update(step); + + // assert + expect(step.content).toEqual('Don\'t you change me!'); + expect(step.error).toBeTruthy(); + expect(step.message).toEqual('Test error'); + }); + + it('should correctly convert valid input', () => { + // arrange + const nextStep: Step = new Step(82); + inputComponentManagerServiceStub.getNext.and.returnValue(nextStep); + const step: Step = testHostComponent.step; + step.content = 'Don\'t you change me!'; + step.error = true; + step.message = 'There be no message.'; + step.selectedConverter = { + getId: () => 'testConverter', + convert: () => 'The Converted Result', + getDisplayname: () => 'Testing converter' + }; + + // act + testHostComponent.sutComponent.update(step); + + // assert + expect(step.content).toEqual('Don\'t you change me!'); + expect(step.error).toBeFalsy(); + expect(step.message).toEqual(''); + expect(nextStep.content).toEqual('The Converted Result'); }); }); From 4d4f8b2992f67a5e341b9e1fbd5829d67026c382 Mon Sep 17 00:00:00 2001 From: Manuel Friedli Date: Fri, 7 Sep 2018 11:30:24 +0200 Subject: [PATCH 06/10] Implement spec for ErrorMessageComponent --- .../error-message.component.spec.ts | 47 +++++++++++++++---- 1 file changed, 37 insertions(+), 10 deletions(-) diff --git a/src/app/error-message/error-message.component.spec.ts b/src/app/error-message/error-message.component.spec.ts index d90c1d5..7b43709 100644 --- a/src/app/error-message/error-message.component.spec.ts +++ b/src/app/error-message/error-message.component.spec.ts @@ -1,25 +1,52 @@ -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import {async, ComponentFixture, TestBed} from '@angular/core/testing'; -import { ErrorMessageComponent } from './error-message.component'; +import {ErrorMessageComponent} from './error-message.component'; +import {Component, ViewChild} from '@angular/core'; +import {Step} from '../step'; + +@Component({ + template: '' +}) +class TestHostComponent { + public step: Step = new Step(42); + @ViewChild(ErrorMessageComponent) + public sutComponent: ErrorMessageComponent; +} describe('ErrorMessageComponent', () => { - let component: ErrorMessageComponent; - let fixture: ComponentFixture; + let testHostComponent: TestHostComponent; + let testHostFixture: ComponentFixture; beforeEach(async(() => { TestBed.configureTestingModule({ - declarations: [ ErrorMessageComponent ] + declarations: [ + ErrorMessageComponent, + TestHostComponent + ] }) - .compileComponents(); + .compileComponents(); })); beforeEach(() => { - fixture = TestBed.createComponent(ErrorMessageComponent); - component = fixture.componentInstance; - fixture.detectChanges(); + testHostFixture = TestBed.createComponent(TestHostComponent); + testHostComponent = testHostFixture.componentInstance; + testHostFixture.detectChanges(); }); it('should create', () => { - expect(component).toBeTruthy(); + expect(testHostComponent.sutComponent).toBeTruthy(); + }); + + it('should display an error message', () => { + // arrange + testHostComponent.step.error = true; + testHostComponent.step.message = 'This is an error message'; + + // act + testHostFixture.detectChanges(); + + // assert + expect(testHostComponent.sutComponent.step.error).toBeTruthy(); + expect(testHostComponent.sutComponent.step.message).toEqual('This is an error message'); }); }); From b634b1d160f2eb6d07e4b0339a94c24e89a9650d Mon Sep 17 00:00:00 2001 From: Manuel Friedli Date: Fri, 7 Sep 2018 12:21:20 +0200 Subject: [PATCH 07/10] Implement tests for ConverterSelectorComponent --- .../converter-selector.component.spec.ts | 88 ++++++++++++++++--- 1 file changed, 78 insertions(+), 10 deletions(-) diff --git a/src/app/converter-selector/converter-selector.component.spec.ts b/src/app/converter-selector/converter-selector.component.spec.ts index 410aaa7..e37fdd0 100644 --- a/src/app/converter-selector/converter-selector.component.spec.ts +++ b/src/app/converter-selector/converter-selector.component.spec.ts @@ -1,25 +1,93 @@ -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import {async, ComponentFixture, TestBed} from '@angular/core/testing'; -import { ConverterSelectorComponent } from './converter-selector.component'; +import {ConverterSelectorComponent} from './converter-selector.component'; +import {Component, ViewChild} from '@angular/core'; +import {Step} from '../step'; +import {TextInputFieldComponent} from '../text-input-field/text-input-field.component'; +import {InputComponentManagerService} from '../input-component-manager.service'; +import {ConverterRegistryService} from '../converter-registry.service'; +import {Converter} from '../converter/converter'; +import createSpyObj = jasmine.createSpyObj; +import SpyObj = jasmine.SpyObj; + +@Component({ + template: '' +}) +class TestHostComponent { + public step: Step = new Step(42); + public textInputComponent: TextInputFieldComponent = new TextInputFieldComponent(inputComponentManagerServiceStub); + @ViewChild(ConverterSelectorComponent) + public sutComponent: ConverterSelectorComponent; +} + +const inputComponentManagerServiceStub = createSpyObj(['getNext']); describe('ConverterSelectorComponent', () => { - let component: ConverterSelectorComponent; - let fixture: ComponentFixture; + let testHostComponent: TestHostComponent; + let testHostFixture: ComponentFixture; + + const converter1: SpyObj = createSpyObj(['getId', 'getDisplayname', 'convert']); + const converter2: SpyObj = createSpyObj(['getId', 'getDisplayname', 'convert']); + const converter3: SpyObj = createSpyObj(['getId', 'getDisplayname', 'convert']); + // converter1.getId.and.returnValue('converter1'); + converter1.getDisplayname.and.returnValue('Converter 1'); + // converter2.getId.and.returnValue('converter2'); + converter2.getDisplayname.and.returnValue('Converter 2'); + // converter3.getId.and.returnValue('converter3'); + converter3.getDisplayname.and.returnValue('Converter 3'); + const converterRegistryServiceStub = createSpyObj(['getAllConverters', 'getConverter']); + converterRegistryServiceStub.getAllConverters.and.returnValue([converter1, converter2, converter3]); + converterRegistryServiceStub.getConverter.and.returnValue(undefined); beforeEach(async(() => { TestBed.configureTestingModule({ - declarations: [ ConverterSelectorComponent ] + declarations: [ + ConverterSelectorComponent, + TestHostComponent + ], + providers: [ + {provide: InputComponentManagerService, useValue: inputComponentManagerServiceStub}, + {provide: ConverterRegistryService, useValue: converterRegistryServiceStub} + ] }) - .compileComponents(); + .compileComponents(); })); beforeEach(() => { - fixture = TestBed.createComponent(ConverterSelectorComponent); - component = fixture.componentInstance; - fixture.detectChanges(); + testHostFixture = TestBed.createComponent(TestHostComponent); + testHostComponent = testHostFixture.componentInstance; + testHostFixture.detectChanges(); }); it('should create', () => { - expect(component).toBeTruthy(); + expect(testHostComponent.sutComponent).toBeTruthy(); + }); + + it('should update the conversion when the selection changes', () => { + // arrange + // set next step + const nextStep: Step = new Step(99); + inputComponentManagerServiceStub.getNext.and.returnValue(nextStep); + // set up converter + const dummyConverter = createSpyObj(['convert']); + dummyConverter.convert.and.returnValue('Converted value'); + converterRegistryServiceStub.getConverter.and.returnValue(dummyConverter); + // create event structure + const event = {target: {selectedOptions: [{id: 'The testing converter id'}]}}; + + // act + testHostComponent.sutComponent.convert(event); + + // assert + expect(converterRegistryServiceStub.getConverter).toHaveBeenCalledWith('The testing converter id'); + expect(nextStep.content).toEqual('Converted value'); + }); + + it('should load available converters upon creation', () => { + const converters: Converter[] = testHostComponent.sutComponent.converters; + expect(converters.length).toEqual(3); + expect(converters[0]).toEqual(converter1); + expect(converters[1]).toEqual(converter2); + expect(converters[2]).toEqual(converter3); }); }); From 7b574d42dfab205e10fe7fbb08eef4f79fc8988a Mon Sep 17 00:00:00 2001 From: Manuel Friedli Date: Fri, 7 Sep 2018 12:21:30 +0200 Subject: [PATCH 08/10] Simplyfy AppComponent spec --- src/app/app.component.spec.ts | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/src/app/app.component.spec.ts b/src/app/app.component.spec.ts index 18d20b6..1a66e4e 100644 --- a/src/app/app.component.spec.ts +++ b/src/app/app.component.spec.ts @@ -1,26 +1,17 @@ import {AppComponent} from './app.component'; import {async, ComponentFixture, TestBed} from '@angular/core/testing'; import {FormsModule} from '@angular/forms'; -import {InputComponentManagerService} from './input-component-manager.service'; import {Step} from './step'; import {VersionComponent} from './version/version.component'; import {ConverterSelectorComponent} from './converter-selector/converter-selector.component'; import {TextInputFieldComponent} from './text-input-field/text-input-field.component'; import {ErrorMessageComponent} from './error-message/error-message.component'; -import createSpyObj = jasmine.createSpyObj; describe('AppComponent', () => { let sut: AppComponent; let fixture: ComponentFixture; const firstStep: Step = new Step(0); - const inputComponentManagerServiceStub = createSpyObj(['getFirst']); - inputComponentManagerServiceStub.getFirst.and.returnValue(firstStep); - - const converterRegistryServiceStub = createSpyObj(['getAllConverters', 'getConverter']); - converterRegistryServiceStub.getAllConverters.and.returnValue([]); - converterRegistryServiceStub.getConverter.and.returnValue(undefined); - beforeEach(async(() => { TestBed.configureTestingModule({ declarations: [ @@ -30,10 +21,7 @@ describe('AppComponent', () => { TextInputFieldComponent, VersionComponent ], - imports: [FormsModule], - providers: [ - {provide: InputComponentManagerService, useValue: inputComponentManagerServiceStub} - ] + imports: [FormsModule] }) .compileComponents(); })); From d9ec7aeb3bea92b099b49000a7852a8942718e1d Mon Sep 17 00:00:00 2001 From: Manuel Friedli Date: Fri, 7 Sep 2018 12:24:07 +0200 Subject: [PATCH 09/10] Link build status badge to build job --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index e0b8242..f2439bf 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ This is a short introduction to the awesome Convertorizr hosted at https://conv.friedli.info/. -Continuous integration is automated with Drone CI (![Build status of develop branch](https://ci.gittr.ch/api/badges/manuel/converter/status.svg?branch=develop)). Usage is self-explanatory. What else do you need to know? +Continuous integration is automated with Drone CI ([![Build status of develop branch](https://ci.gittr.ch/api/badges/manuel/converter/status.svg?branch=develop)](https://ci.gittr.ch/manuel/converter)). Usage is self-explanatory. What else do you need to know? The source code is hosted at https://gittr.ch/manuel/converter.git. Contact the author at manuel-convertorizr|at|fritteli.ch. From 1c6c7d556e7331f56ff1e313bb24a27dc2ceda99 Mon Sep 17 00:00:00 2001 From: Manuel Friedli Date: Fri, 7 Sep 2018 12:36:53 +0200 Subject: [PATCH 10/10] Add coverage option to tests --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 3c55b59..a095b9a 100644 --- a/package.json +++ b/package.json @@ -24,8 +24,8 @@ "start": "ng serve", "build": "ng build --delete-output-path", "build-prod": "ng build --prod --optimization --aot --delete-output-path --build-optimizer", - "test": "ng test", - "test:ci": "ng test --browsers ChromeHeadlessNoSandbox --watch=false", + "test": "ng test --code-coverage", + "test:ci": "ng test --browsers ChromeHeadlessNoSandbox --watch=false --code-coverage", "lint": "ng lint", "e2e": "ng e2e", "postinstall": "npm rebuild node-sass"