Merge branch 'feature/collapsible-intermediate-input' of manuel/converter into develop
All checks were successful
continuous-integration/drone the build was successful

This commit is contained in:
Manuel Friedli 2018-09-07 12:49:20 +02:00 committed by Gitea
commit 7ca8863699
20 changed files with 537 additions and 187 deletions

View file

@ -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.

View file

@ -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"

View file

@ -1,18 +1,7 @@
<div *ngFor="let step of steps" class="inputwrapper">
<div class="textwrapper arrow_box">
<textarea class="textinput" (keyup)="update(step)" placeholder="Please enter your input ..."
[(ngModel)]="step.content">{{step.content}}</textarea>
</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>
<app-text-input-field [step]="step" #ti></app-text-input-field>
<app-converter-selector [step]="step" [textInput]="ti"></app-converter-selector>
<app-error-message [step]="step"></app-error-message>
</div>
<app-version></app-version>
<!--<router-outlet></router-outlet>-->

View file

@ -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;
}

View file

@ -1,32 +1,27 @@
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 {ConverterRegistryService} from './converter-registry.service';
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', () => {
let sut: AppComponent;
let fixture: ComponentFixture<AppComponent>;
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: [AppComponent, VersionComponent],
imports: [FormsModule],
providers: [
{provide: InputComponentManagerService, useValue: inputComponentManagerServiceStub},
{provide: ConverterRegistryService, useValue: converterRegistryServiceStub}
]
declarations: [
AppComponent,
ConverterSelectorComponent,
ErrorMessageComponent,
TextInputFieldComponent,
VersionComponent
],
imports: [FormsModule]
})
.compileComponents();
}));

View file

@ -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();
}

View file

@ -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,

View file

@ -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);
}
}

View file

@ -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>

View 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;*/
}

View file

@ -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);
});
});

View 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();
}
}

View file

@ -0,0 +1 @@
<div class="errormessage" *ngIf="step.error" [innerHTML]="step.message"></div>

View file

@ -0,0 +1,4 @@
.errormessage {
color: red;
text-align: center;
}

View 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');
});
});

View 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() {
}
}

View 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>

View 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;
}

View 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');
});
});

View 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);
}
}
}
}
}