Merge branch 'feature/collapsible-intermediate-input' of manuel/converter into develop
All checks were successful
continuous-integration/drone the build was successful
All checks were successful
continuous-integration/drone the build was successful
This commit is contained in:
commit
7ca8863699
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/.
|
||||
|
||||
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.
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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>-->
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}));
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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