Add component tests #8
					 20 changed files with 537 additions and 187 deletions
				
			
		|  | @ -2,7 +2,7 @@ | ||||||
| 
 | 
 | ||||||
| This is a short introduction to the awesome Convertorizr hosted at https://conv.friedli.info/. | This is a short introduction to the awesome Convertorizr hosted at https://conv.friedli.info/. | ||||||
| 
 | 
 | ||||||
| Continuous integration is automated with Drone CI (). Usage is self-explanatory. What else do you need to know? | Continuous integration is automated with Drone CI ([](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. | The source code is hosted at https://gittr.ch/manuel/converter.git. | ||||||
| Contact the author at manuel-convertorizr|at|fritteli.ch. | Contact the author at manuel-convertorizr|at|fritteli.ch. | ||||||
|  |  | ||||||
|  | @ -24,8 +24,8 @@ | ||||||
|     "start": "ng serve", |     "start": "ng serve", | ||||||
|     "build": "ng build --delete-output-path", |     "build": "ng build --delete-output-path", | ||||||
|     "build-prod": "ng build --prod --optimization --aot --delete-output-path --build-optimizer", |     "build-prod": "ng build --prod --optimization --aot --delete-output-path --build-optimizer", | ||||||
|     "test": "ng test", |     "test": "ng test --code-coverage", | ||||||
|     "test:ci": "ng test --browsers ChromeHeadlessNoSandbox --watch=false", |     "test:ci": "ng test --browsers ChromeHeadlessNoSandbox --watch=false --code-coverage", | ||||||
|     "lint": "ng lint", |     "lint": "ng lint", | ||||||
|     "e2e": "ng e2e", |     "e2e": "ng e2e", | ||||||
|     "postinstall": "npm rebuild node-sass" |     "postinstall": "npm rebuild node-sass" | ||||||
|  |  | ||||||
|  | @ -1,18 +1,7 @@ | ||||||
| <div *ngFor="let step of steps" class="inputwrapper"> | <div *ngFor="let step of steps" class="inputwrapper"> | ||||||
|   <div class="textwrapper arrow_box"> |   <app-text-input-field [step]="step" #ti></app-text-input-field> | ||||||
|     <textarea class="textinput" (keyup)="update(step)" placeholder="Please enter your input ..." |   <app-converter-selector [step]="step" [textInput]="ti"></app-converter-selector> | ||||||
|               [(ngModel)]="step.content">{{step.content}}</textarea> |   <app-error-message [step]="step"></app-error-message> | ||||||
|   </div> |  | ||||||
|   <div [ngClass]="{selectwrapper: true, error: step.error}"> |  | ||||||
|     <div class="arrow_box"> |  | ||||||
|       <select class="select" (change)="convert(step, $event)"> |  | ||||||
|         <option id="undefined">Select conversion ...</option> |  | ||||||
|         <option class="option" *ngFor="let c of converters" id="{{c.getId()}}">{{c.getDisplayname()}} |  | ||||||
|         </option> |  | ||||||
|       </select> |  | ||||||
|     </div> |  | ||||||
|   </div> |  | ||||||
|   <div class="errormessage" *ngIf="step.error" [innerHTML]="step.message"></div> |  | ||||||
| </div> | </div> | ||||||
| <app-version></app-version> | <app-version></app-version> | ||||||
| <!--<router-outlet></router-outlet>--> | <!--<router-outlet></router-outlet>--> | ||||||
|  |  | ||||||
|  | @ -2,106 +2,3 @@ | ||||||
|   font-family: "ABeeZee", sans-serif; |   font-family: "ABeeZee", sans-serif; | ||||||
|   margin: 0 1em 1em 1em; |   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; |  | ||||||
| } |  | ||||||
|  |  | ||||||
|  | @ -1,32 +1,27 @@ | ||||||
| import {AppComponent} from './app.component'; | import {AppComponent} from './app.component'; | ||||||
| import {async, ComponentFixture, TestBed} from '@angular/core/testing'; | import {async, ComponentFixture, TestBed} from '@angular/core/testing'; | ||||||
| import {FormsModule} from '@angular/forms'; | import {FormsModule} from '@angular/forms'; | ||||||
| import {InputComponentManagerService} from './input-component-manager.service'; |  | ||||||
| import {Step} from './step'; | import {Step} from './step'; | ||||||
| import {ConverterRegistryService} from './converter-registry.service'; |  | ||||||
| import {VersionComponent} from './version/version.component'; | import {VersionComponent} from './version/version.component'; | ||||||
| import createSpyObj = jasmine.createSpyObj; | 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'; | ||||||
| 
 | 
 | ||||||
| describe('AppComponent', () => { | describe('AppComponent', () => { | ||||||
|   let sut: AppComponent; |   let sut: AppComponent; | ||||||
|   let fixture: ComponentFixture<AppComponent>; |   let fixture: ComponentFixture<AppComponent>; | ||||||
|   const firstStep: Step = new Step(0); |   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(() => { |   beforeEach(async(() => { | ||||||
|     TestBed.configureTestingModule({ |     TestBed.configureTestingModule({ | ||||||
|       declarations: [AppComponent, VersionComponent], |       declarations: [ | ||||||
|       imports: [FormsModule], |         AppComponent, | ||||||
|       providers: [ |         ConverterSelectorComponent, | ||||||
|         {provide: InputComponentManagerService, useValue: inputComponentManagerServiceStub}, |         ErrorMessageComponent, | ||||||
|         {provide: ConverterRegistryService, useValue: converterRegistryServiceStub} |         TextInputFieldComponent, | ||||||
|       ] |         VersionComponent | ||||||
|  |       ], | ||||||
|  |       imports: [FormsModule] | ||||||
|     }) |     }) | ||||||
|       .compileComponents(); |       .compileComponents(); | ||||||
|   })); |   })); | ||||||
|  |  | ||||||
|  | @ -1,8 +1,6 @@ | ||||||
| import {Component, OnInit} from '@angular/core'; | import {Component, OnInit} from '@angular/core'; | ||||||
| import {ConverterRegistryService} from './converter-registry.service'; |  | ||||||
| import {InputComponentManagerService} from './input-component-manager.service'; | import {InputComponentManagerService} from './input-component-manager.service'; | ||||||
| import {Step} from './step'; | import {Step} from './step'; | ||||||
| import {Converter} from './converter/converter'; |  | ||||||
| 
 | 
 | ||||||
| @Component({ | @Component({ | ||||||
|   selector: 'app-root', |   selector: 'app-root', | ||||||
|  | @ -11,47 +9,11 @@ import {Converter} from './converter/converter'; | ||||||
| }) | }) | ||||||
| export class AppComponent implements OnInit { | export class AppComponent implements OnInit { | ||||||
|   public steps: Step[] = []; |   public steps: Step[] = []; | ||||||
|   public converters: Converter[] = []; |  | ||||||
| 
 | 
 | ||||||
|   constructor(private converterRegistryService: ConverterRegistryService, |   constructor(private inputComponentManagerService: InputComponentManagerService) { | ||||||
|               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); |  | ||||||
|         } |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   ngOnInit(): void { |   ngOnInit(): void { | ||||||
|     this.converters = this.converterRegistryService.getAllConverters(); |  | ||||||
|     this.steps = this.inputComponentManagerService.getAllComponents(); |     this.steps = this.inputComponentManagerService.getAllComponents(); | ||||||
|     this.inputComponentManagerService.getFirst(); |     this.inputComponentManagerService.getFirst(); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  | @ -6,11 +6,17 @@ import {InputComponentManagerService} from './input-component-manager.service'; | ||||||
| import {NativeLibraryWrapperService} from './native-library-wrapper.service'; | import {NativeLibraryWrapperService} from './native-library-wrapper.service'; | ||||||
| import {FormsModule} from '@angular/forms'; | import {FormsModule} from '@angular/forms'; | ||||||
| import {VersionComponent} from './version/version.component'; | 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({ | @NgModule({ | ||||||
|   declarations: [ |   declarations: [ | ||||||
|     AppComponent, |     AppComponent, | ||||||
|     VersionComponent |     VersionComponent, | ||||||
|  |     TextInputFieldComponent, | ||||||
|  |     ConverterSelectorComponent, | ||||||
|  |     ErrorMessageComponent | ||||||
|   ], |   ], | ||||||
|   imports: [ |   imports: [ | ||||||
|     BrowserModule, |     BrowserModule, | ||||||
|  |  | ||||||
|  | @ -36,12 +36,7 @@ export class ConverterRegistryService { | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   public getConverter(id: string): Converter { |   public getConverter(id: string): Converter { | ||||||
|     for (let i = 0; i < this.converters.length; i++) { |     return this.converters.find((converter: Converter): boolean => converter.getId() === id); | ||||||
|       if (this.converters[i].getId() === id) { |  | ||||||
|         return this.converters[i]; |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|     return undefined; |  | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   private init(): void { |   private init(): void { | ||||||
|  | @ -67,11 +62,11 @@ export class ConverterRegistryService { | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   private registerConverter(converter: Converter): void { |   private registerConverter(converter: Converter): void { | ||||||
|     this.converters.forEach((c: Converter) => { |     // Don't allow duplicate registration of the same converter id
 | ||||||
|       if (c.getId() === converter.getId()) { |     if (this.converters.some((c: Converter): boolean => c.getId() === converter.getId())) { | ||||||
|         throw new Error('Converter-ID ' + converter.getId() + ' is already registered!'); |       throw new Error(`Converter-ID ${converter.getId()} is already registered!`); | ||||||
|       } |     } | ||||||
|     }); | 
 | ||||||
|     this.converters.push(converter); |     this.converters.push(converter); | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -0,0 +1,9 @@ | ||||||
|  | <div [ngClass]="{selectwrapper: true, error: step.error}"> | ||||||
|  |   <div class="arrow_box"> | ||||||
|  |     <select class="select" (change)="convert($event)"> | ||||||
|  |       <option id="undefined">Select conversion ...</option> | ||||||
|  |       <option class="option" *ngFor="let c of converters" id="{{c.getId()}}">{{c.getDisplayname()}} | ||||||
|  |       </option> | ||||||
|  |     </select> | ||||||
|  |   </div> | ||||||
|  | </div> | ||||||
							
								
								
									
										74
									
								
								src/app/converter-selector/converter-selector.component.scss
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										74
									
								
								src/app/converter-selector/converter-selector.component.scss
									
										
									
									
									
										Normal file
									
								
							|  | @ -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;*/ | ||||||
|  | } | ||||||
|  | @ -0,0 +1,93 @@ | ||||||
|  | import {async, ComponentFixture, TestBed} from '@angular/core/testing'; | ||||||
|  | 
 | ||||||
|  | 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: '<app-converter-selector [step]="step" [textInput]="textInputComponent"></app-converter-selector>' | ||||||
|  | }) | ||||||
|  | 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 testHostComponent: TestHostComponent; | ||||||
|  |   let testHostFixture: ComponentFixture<TestHostComponent>; | ||||||
|  | 
 | ||||||
|  |   const converter1: SpyObj<Converter> = createSpyObj(['getId', 'getDisplayname', 'convert']); | ||||||
|  |   const converter2: SpyObj<Converter> = createSpyObj(['getId', 'getDisplayname', 'convert']); | ||||||
|  |   const converter3: SpyObj<Converter> = 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, | ||||||
|  |         TestHostComponent | ||||||
|  |       ], | ||||||
|  |       providers: [ | ||||||
|  |         {provide: InputComponentManagerService, useValue: inputComponentManagerServiceStub}, | ||||||
|  |         {provide: ConverterRegistryService, useValue: converterRegistryServiceStub} | ||||||
|  |       ] | ||||||
|  |     }) | ||||||
|  |       .compileComponents(); | ||||||
|  |   })); | ||||||
|  | 
 | ||||||
|  |   beforeEach(() => { | ||||||
|  |     testHostFixture = TestBed.createComponent(TestHostComponent); | ||||||
|  |     testHostComponent = testHostFixture.componentInstance; | ||||||
|  |     testHostFixture.detectChanges(); | ||||||
|  |   }); | ||||||
|  | 
 | ||||||
|  |   it('should create', () => { | ||||||
|  |     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); | ||||||
|  |   }); | ||||||
|  | }); | ||||||
							
								
								
									
										30
									
								
								src/app/converter-selector/converter-selector.component.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								src/app/converter-selector/converter-selector.component.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -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($event: any): void { | ||||||
|  |     this.step.selectedConverter = this.converterRegistryService.getConverter($event.target.selectedOptions[0].id); | ||||||
|  |     this.textInput.update(this.step); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   ngOnInit() { | ||||||
|  |     this.converters = this.converterRegistryService.getAllConverters(); | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										1
									
								
								src/app/error-message/error-message.component.html
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								src/app/error-message/error-message.component.html
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1 @@ | ||||||
|  | <div class="errormessage" *ngIf="step.error" [innerHTML]="step.message"></div> | ||||||
							
								
								
									
										4
									
								
								src/app/error-message/error-message.component.scss
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								src/app/error-message/error-message.component.scss
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,4 @@ | ||||||
|  | .errormessage { | ||||||
|  |   color: red; | ||||||
|  |   text-align: center; | ||||||
|  | } | ||||||
							
								
								
									
										52
									
								
								src/app/error-message/error-message.component.spec.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										52
									
								
								src/app/error-message/error-message.component.spec.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,52 @@ | ||||||
|  | import {async, ComponentFixture, TestBed} from '@angular/core/testing'; | ||||||
|  | 
 | ||||||
|  | import {ErrorMessageComponent} from './error-message.component'; | ||||||
|  | import {Component, ViewChild} from '@angular/core'; | ||||||
|  | import {Step} from '../step'; | ||||||
|  | 
 | ||||||
|  | @Component({ | ||||||
|  |   template: '<app-error-message [step]="step"></app-error-message>' | ||||||
|  | }) | ||||||
|  | class TestHostComponent { | ||||||
|  |   public step: Step = new Step(42); | ||||||
|  |   @ViewChild(ErrorMessageComponent) | ||||||
|  |   public sutComponent: ErrorMessageComponent; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | describe('ErrorMessageComponent', () => { | ||||||
|  |   let testHostComponent: TestHostComponent; | ||||||
|  |   let testHostFixture: ComponentFixture<TestHostComponent>; | ||||||
|  | 
 | ||||||
|  |   beforeEach(async(() => { | ||||||
|  |     TestBed.configureTestingModule({ | ||||||
|  |       declarations: [ | ||||||
|  |         ErrorMessageComponent, | ||||||
|  |         TestHostComponent | ||||||
|  |       ] | ||||||
|  |     }) | ||||||
|  |       .compileComponents(); | ||||||
|  |   })); | ||||||
|  | 
 | ||||||
|  |   beforeEach(() => { | ||||||
|  |     testHostFixture = TestBed.createComponent(TestHostComponent); | ||||||
|  |     testHostComponent = testHostFixture.componentInstance; | ||||||
|  |     testHostFixture.detectChanges(); | ||||||
|  |   }); | ||||||
|  | 
 | ||||||
|  |   it('should create', () => { | ||||||
|  |     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'); | ||||||
|  |   }); | ||||||
|  | }); | ||||||
							
								
								
									
										15
									
								
								src/app/error-message/error-message.component.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								src/app/error-message/error-message.component.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -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() { | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										4
									
								
								src/app/text-input-field/text-input-field.component.html
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								src/app/text-input-field/text-input-field.component.html
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,4 @@ | ||||||
|  | <div class="textwrapper arrow_box"> | ||||||
|  |     <textarea class="textinput" (keyup)="update(step)" placeholder="Please enter your input ..." | ||||||
|  |               [(ngModel)]="step.content">{{step.content}}</textarea> | ||||||
|  | </div> | ||||||
							
								
								
									
										69
									
								
								src/app/text-input-field/text-input-field.component.scss
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										69
									
								
								src/app/text-input-field/text-input-field.component.scss
									
										
									
									
									
										Normal file
									
								
							|  | @ -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; | ||||||
|  | } | ||||||
							
								
								
									
										110
									
								
								src/app/text-input-field/text-input-field.component.spec.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										110
									
								
								src/app/text-input-field/text-input-field.component.spec.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,110 @@ | ||||||
|  | import {async, ComponentFixture, TestBed} from '@angular/core/testing'; | ||||||
|  | 
 | ||||||
|  | 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: '<app-text-input-field [step]="step"></app-text-input-field>' | ||||||
|  | }) | ||||||
|  | class TestHostComponent { | ||||||
|  |   public step: Step = new Step(42); | ||||||
|  |   @ViewChild(TextInputFieldComponent) | ||||||
|  |   public sutComponent: TextInputFieldComponent; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | describe('TextInputFieldComponent', () => { | ||||||
|  |   let testHostComponent: TestHostComponent; | ||||||
|  |   let testHostFixture: ComponentFixture<TestHostComponent>; | ||||||
|  | 
 | ||||||
|  |   const inputComponentManagerServiceStub = createSpyObj(['getNext']); | ||||||
|  |   inputComponentManagerServiceStub.getNext.and.returnValue(undefined); | ||||||
|  | 
 | ||||||
|  |   beforeEach(async(() => { | ||||||
|  |     TestBed.configureTestingModule({ | ||||||
|  |       declarations: [ | ||||||
|  |         TestHostComponent, | ||||||
|  |         TextInputFieldComponent | ||||||
|  |       ], | ||||||
|  |       imports: [FormsModule], | ||||||
|  |       providers: [{provide: InputComponentManagerService, useValue: inputComponentManagerServiceStub}] | ||||||
|  |     }) | ||||||
|  |       .compileComponents(); | ||||||
|  |   })); | ||||||
|  | 
 | ||||||
|  |   beforeEach(() => { | ||||||
|  |     testHostFixture = TestBed.createComponent(TestHostComponent); | ||||||
|  |     testHostComponent = testHostFixture.componentInstance; | ||||||
|  |     testHostFixture.detectChanges(); | ||||||
|  |   }); | ||||||
|  | 
 | ||||||
|  |   it('should create', () => { | ||||||
|  |     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'); | ||||||
|  |   }); | ||||||
|  | }); | ||||||
							
								
								
									
										45
									
								
								src/app/text-input-field/text-input-field.component.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								src/app/text-input-field/text-input-field.component.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -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); | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue