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 (![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.
|
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…
Reference in a new issue