Merge branch 'develop'
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
Manuel Friedli 2024-01-28 00:19:06 +01:00
commit 59fdaed9f9
Signed by: manuel
GPG key ID: 41D08ABA75634DA1
96 changed files with 13140 additions and 11184 deletions

12
.drone.yml Normal file
View file

@ -0,0 +1,12 @@
kind: pipeline
type: docker
name: default
steps:
- name: install
image: node:20-alpine
commands:
- apk add firefox
- npm install
- npm run test:ci
- npm run build

View file

@ -1,4 +1,4 @@
# Editor configuration, see http://editorconfig.org # Editor configuration, see https://editorconfig.org
root = true root = true
[*] [*]
@ -8,6 +8,9 @@ indent_size = 2
insert_final_newline = true insert_final_newline = true
trim_trailing_whitespace = true trim_trailing_whitespace = true
[*.ts]
quote_type = single
[*.md] [*.md]
max_line_length = off max_line_length = off
trim_trailing_whitespace = false trim_trailing_whitespace = false

28
.gitignore vendored
View file

@ -1,44 +1,42 @@
# See http://help.github.com/ignore-files/ for more about ignoring files. # See http://help.github.com/ignore-files/ for more about ignoring files.
# compiled output # Compiled output
/dist /dist
/tmp /tmp
/out-tsc /out-tsc
/bazel-out
# dependencies # Node
/node_modules /node_modules
npm-debug.log
yarn-error.log
# IDEs and editors # IDEs and editors
/.idea .idea/
.project .project
.classpath .classpath
.c9/ .c9/
*.launch *.launch
.settings/ .settings/
*.sublime-workspace *.sublime-workspace
*.iml
# IDE - VSCode # Visual Studio Code
.vscode/* .vscode/tasks.json
!.vscode/settings.json !.vscode/settings.json
!.vscode/tasks.json !.vscode/tasks.json
!.vscode/launch.json !.vscode/launch.json
!.vscode/extensions.json !.vscode/extensions.json
.history/*
# misc # Miscellaneous
/.sass-cache /.angular/cache
.sass-cache/
/connect.lock /connect.lock
/coverage /coverage
/libpeerconnection.log /libpeerconnection.log
npm-debug.log
testem.log testem.log
/typings /typings
# e2e # System files
/e2e/*.js
/e2e/*.map
# System Files
.DS_Store .DS_Store
Thumbs.db Thumbs.db
*~

View file

@ -1,8 +1,8 @@
# convertorizr - Convert whatever you want! # Convertorizr - Convert whatever you want!
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/.
Deployment is automated with Gitlab CI. Usage is self-explanatory. What else do you need to know? Continuous integration is automated with Drone CI ([![Build Status](https://ci.gittr.ch/api/badges/manuel/converter/status.svg)](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.
@ -11,11 +11,11 @@ Cheers!
# Technical stuff # Technical stuff
This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 6.1.5. This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 17.1.0.
## Development server ## Development server
Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The app will automatically reload if you change any of the source files. Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The application will automatically reload if you change any of the source files.
## Code scaffolding ## Code scaffolding
@ -23,7 +23,7 @@ Run `ng generate component component-name` to generate a new component. You can
## Build ## Build
Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. Use the `--prod` flag for a production build. Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory.
## Running unit tests ## Running unit tests
@ -31,8 +31,8 @@ Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.
## Running end-to-end tests ## Running end-to-end tests
Run `ng e2e` to execute the end-to-end tests via [Protractor](http://www.protractortest.org/). Run `ng e2e` to execute the end-to-end tests via a platform of your choice. To use this command, you need to first add a package that implements end-to-end testing capabilities.
## Further help ## Further help
To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI README](https://github.com/angular/angular-cli/blob/master/README.md). To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI Overview and Command Reference](https://angular.io/cli) page.

View file

@ -3,21 +3,28 @@
"version": 1, "version": 1,
"newProjectRoot": "projects", "newProjectRoot": "projects",
"projects": { "projects": {
"converter": { "convertorizr": {
"projectType": "application",
"schematics": {
"@schematics/angular:component": {
"style": "scss"
}
},
"root": "", "root": "",
"sourceRoot": "src", "sourceRoot": "src",
"projectType": "application",
"prefix": "app", "prefix": "app",
"schematics": {},
"architect": { "architect": {
"build": { "build": {
"builder": "@angular-devkit/build-angular:browser", "builder": "@angular-devkit/build-angular:application",
"options": { "options": {
"outputPath": "dist/converter", "outputPath": "dist/convertorizr",
"index": "src/index.html", "index": "src/index.html",
"main": "src/main.ts", "browser": "src/main.ts",
"polyfills": "src/polyfills.ts", "polyfills": [
"tsConfig": "src/tsconfig.app.json", "zone.js"
],
"tsConfig": "tsconfig.app.json",
"inlineStyleLanguage": "scss",
"assets": [ "assets": [
"src/favicon.ico", "src/favicon.ico",
"src/assets" "src/assets"
@ -29,99 +36,67 @@
}, },
"configurations": { "configurations": {
"production": { "production": {
"fileReplacements": [ "budgets": [
{ {
"replace": "src/environments/environment.ts", "type": "initial",
"with": "src/environments/environment.prod.ts" "maximumWarning": "500kb",
"maximumError": "1mb"
},
{
"type": "anyComponentStyle",
"maximumWarning": "2kb",
"maximumError": "4kb"
} }
], ],
"optimization": true, "outputHashing": "all"
"outputHashing": "all", },
"sourceMap": false, "development": {
"extractCss": true, "optimization": false,
"namedChunks": false, "extractLicenses": false,
"aot": true, "sourceMap": true
"extractLicenses": true,
"vendorChunk": false,
"buildOptimizer": true
}
} }
}, },
"defaultConfiguration": "production"
},
"serve": { "serve": {
"builder": "@angular-devkit/build-angular:dev-server", "builder": "@angular-devkit/build-angular:dev-server",
"options": {
"browserTarget": "converter:build"
},
"configurations": { "configurations": {
"production": { "production": {
"browserTarget": "converter:build:production" "buildTarget": "convertorizr:build:production"
} },
"development": {
"buildTarget": "convertorizr:build:development"
} }
}, },
"defaultConfiguration": "development"
},
"extract-i18n": { "extract-i18n": {
"builder": "@angular-devkit/build-angular:extract-i18n", "builder": "@angular-devkit/build-angular:extract-i18n",
"options": { "options": {
"browserTarget": "converter:build" "buildTarget": "convertorizr:build"
} }
}, },
"test": { "test": {
"builder": "@angular-devkit/build-angular:karma", "builder": "@angular-devkit/build-angular:karma",
"options": { "options": {
"main": "src/test.ts", "polyfills": [
"polyfills": "src/polyfills.ts", "zone.js",
"tsConfig": "src/tsconfig.spec.json", "zone.js/testing"
"karmaConfig": "src/karma.conf.js", ],
"tsConfig": "tsconfig.spec.json",
"inlineStyleLanguage": "scss",
"assets": [
"src/favicon.ico",
"src/assets"
],
"styles": [ "styles": [
"src/styles.scss" "src/styles.scss"
], ],
"scripts": [], "scripts": [],
"assets": [ "karmaConfig": "karma.conf.js"
"src/favicon.ico", }
"src/assets"
]
}
},
"lint": {
"builder": "@angular-devkit/build-angular:tslint",
"options": {
"tsConfig": [
"src/tsconfig.app.json",
"src/tsconfig.spec.json"
],
"exclude": [
"**/node_modules/**"
]
}
}
}
},
"converter-e2e": {
"root": "e2e/",
"projectType": "application",
"architect": {
"e2e": {
"builder": "@angular-devkit/build-angular:protractor",
"options": {
"protractorConfig": "e2e/protractor.conf.js",
"devServerTarget": "converter:serve"
},
"configurations": {
"production": {
"devServerTarget": "converter:serve:production"
}
}
},
"lint": {
"builder": "@angular-devkit/build-angular:tslint",
"options": {
"tsConfig": "e2e/tsconfig.e2e.json",
"exclude": [
"**/node_modules/**"
]
} }
} }
} }
} }
},
"defaultProject": "converter"
} }

View file

@ -1,31 +0,0 @@
import {ConvertorizrPage} from './app.po';
describe('convertorizr App', () => {
let page: ConvertorizrPage;
beforeEach(() => {
page = new ConvertorizrPage();
});
it('should display a textarea that is initially empty', () => {
page.navigateTo()
.then(() => page.getInputFieldContent(0))
.then((value: string) => {
expect(value).toEqual('');
});
});
it('should convert a string to its base64 representation', () => {
page.navigateTo()
.then(() => page.setInputFieldContent(0, 'Hello, World!'))
.then(() => page.getSelectedConverterOption(0))
.then((option: string) => {
expect(option).toEqual('Select conversion ...');
})
.then(() => page.selectConverterOption(0, 'Encode Base 64'))
.then(() => page.getInputFieldContent(1))
.then((content: string) => {
expect(content).toEqual('SGVsbG8sIFdvcmxkIQ==');
});
});
});

View file

@ -1,44 +0,0 @@
import {browser, by, element, ElementFinder} from 'protractor';
import {promise, WebElementPromise} from 'selenium-webdriver';
export class ConvertorizrPage {
navigateTo(): promise.Promise<any> {
return browser.get('/');
}
private getInputField(index: number): WebElementPromise {
return element
.all(by.css('app-root div.inputwrapper'))
.get(index)
.element(by.css('.textwrapper textarea'))
.getWebElement();
}
getInputFieldContent(index: number): promise.Promise<string> {
return this.getInputField(index).getText();
}
setInputFieldContent(index: number, content: string): promise.Promise<void> {
return this.getInputField(index).sendKeys(content);
}
private getConverterDropdown(index: number): ElementFinder {
return element
.all(by.css('app-root div.inputwrapper'))
.get(index)
.element(by.css('.selectwrapper select'));
}
getSelectedConverterOption(index: number): promise.Promise<string> {
return this.getConverterDropdown(index)
.$('option:checked')
.getWebElement()
.getText();
}
selectConverterOption(index: number, optionName: string): promise.Promise<void> {
return this.getConverterDropdown(index)
.element(by.cssContainingText('option', optionName))
.click();
}
}

View file

@ -1,13 +0,0 @@
{
"extends": "../tsconfig.json",
"compilerOptions": {
"outDir": "../out-tsc/app",
"module": "commonjs",
"target": "es5",
"types": [
"jasmine",
"jasminewd2",
"node"
]
}
}

40
karma.conf.js Normal file
View file

@ -0,0 +1,40 @@
// Karma configuration file, see link for more information
// https://karma-runner.github.io/1.0/config/configuration-file.html
module.exports = function (config) {
config.set({
basePath: '',
frameworks: ['jasmine', '@angular-devkit/build-angular'],
plugins: [
require('karma-jasmine'),
require('karma-chrome-launcher'),
require('karma-firefox-launcher'),
require('karma-jasmine-html-reporter'),
require('karma-coverage'),
require('@angular-devkit/build-angular/plugins/karma')
],
client: {
jasmine: {
// you can add configuration options for Jasmine here
// the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html
// for example, you can disable the random execution with `random: false`
// or set a specific seed with `seed: 4321`
},
clearContext: false // leave Jasmine Spec Runner output visible in browser
},
jasmineHtmlReporter: {
suppressAll: true // removes the duplicated traces
},
coverageReporter: {
dir: require('path').join(__dirname, './coverage/convertorizr'),
subdir: '.',
reporters: [
{ type: 'html' },
{ type: 'text-summary' }
]
},
reporters: ['progress', 'kjhtml'],
browsers: ['Firefox'],
restartOnFileChange: true
});
};

22147
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,6 @@
{ {
"name": "convertorizr", "name": "convertorizr",
"version": "1.3.0", "version": "2.0.0-dev.1",
"description": "Decode or encode base64, punycode, HTML entities, URI components, ...", "description": "Decode or encode base64, punycode, HTML entities, URI components, ...",
"keywords": [ "keywords": [
"encode", "encode",
@ -14,7 +14,7 @@
"email": "manuel@fritteli.ch" "email": "manuel@fritteli.ch"
}, },
"license": "MIT", "license": "MIT",
"homepage": "https://manuel.pages.gittr.ch/dencode.org", "homepage": "https://conv.friedli.info/",
"repository": { "repository": {
"type": "git", "type": "git",
"url": "https://gittr.ch/manuel/converter.git" "url": "https://gittr.ch/manuel/converter.git"
@ -22,47 +22,44 @@
"scripts": { "scripts": {
"ng": "ng", "ng": "ng",
"start": "ng serve", "start": "ng serve",
"build": "ng build --delete-output-path", "build": "ng build",
"build-prod": "ng build --prod --optimization --aot --delete-output-path --build-optimizer", "watch": "ng build --watch --configuration development",
"test": "ng test", "test": "ng test --browsers=Chromium,Firefox",
"lint": "ng lint", "test:ci": "ng test --no-watch --no-progress --browsers=FirefoxHeadless"
"e2e": "ng e2e",
"postinstall": "npm rebuild node-sass"
}, },
"private": true, "private": true,
"dependencies": { "dependencies": {
"@angular/common": "6.1.6", "@angular/animations": "^17.1.0",
"@angular/compiler": "6.1.6", "@angular/common": "^17.1.0",
"@angular/core": "6.1.6", "@angular/compiler": "^17.1.0",
"@angular/forms": "6.1.6", "@angular/core": "^17.1.0",
"@angular/platform-browser": "6.1.6", "@angular/forms": "^17.1.0",
"@angular/platform-browser-dynamic": "6.1.6", "@angular/platform-browser": "^17.1.0",
"@angular/router": "6.1.6", "@angular/platform-browser-dynamic": "^17.1.0",
"core-js": "^2.5.1", "@angular/router": "^17.1.0",
"punycode": "^2.1.0", "punycode": "^2.3.1",
"quoted-printable": "^1.0.0", "quoted-printable": "^1.0.1",
"rxjs": "^6.3.0", "rxjs": "~7.8.0",
"utf8": "^2.1.0", "tslib": "^2.3.0",
"zone.js": "^0.8.26" "utf8": "^3.0.0",
"zone.js": "~0.14.3"
}, },
"devDependencies": { "devDependencies": {
"@angular-devkit/build-angular": "~0.7.0", "@angular-devkit/build-angular": "^17.1.0",
"@angular/cli": "6.1.5", "@angular/cli": "^17.1.0",
"@angular/compiler-cli": "6.1.6", "@angular/compiler-cli": "^17.1.0",
"@types/jasmine": "~2.8.6", "@types/jasmine": "~5.1.0",
"@types/jasminewd2": "~2.0.3", "@types/node": "^20.11.5",
"@types/node": "~8.9.4", "@types/punycode": "^2.1.3",
"codelyzer": "~4.2.1", "@types/quoted-printable": "^1.0.2",
"jasmine-core": "~2.99.1", "@types/utf8": "^3.0.3",
"jasmine-spec-reporter": "~4.2.1", "jasmine-core": "~5.1.0",
"karma": "~1.7.1", "karma": "~6.4.0",
"karma-chrome-launcher": "~2.2.0", "karma-chrome-launcher": "~3.2.0",
"karma-coverage-istanbul-reporter": "~2.0.0", "karma-coverage": "~2.2.0",
"karma-jasmine": "~1.1.1", "karma-firefox-launcher": "^2.1.2",
"karma-jasmine-html-reporter": "^0.2.2", "karma-jasmine": "~5.1.0",
"protractor": "~5.4.0", "karma-jasmine-html-reporter": "~2.1.0",
"ts-node": "~5.0.1", "typescript": "~5.3.2"
"tslint": "~5.9.1",
"typescript": "2.9.2"
} }
} }

View file

@ -1,16 +0,0 @@
import {NgModule} from '@angular/core';
import {RouterModule, Routes} from '@angular/router';
const routes: Routes = [
{
path: '',
children: []
}
];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule]
})
export class AppRoutingModule {
}

View file

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

View file

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

View file

@ -1,53 +1,26 @@
import {AppComponent} from './app.component'; import {AppComponent} from './app.component';
import {async, ComponentFixture, TestBed} from '@angular/core/testing'; import {ComponentFixture, TestBed} from '@angular/core/testing';
import {FormsModule} from '@angular/forms';
import {InputComponentManagerService} from './inputcomponentmanager.service';
import {Step} from './step'; import {Step} from './step';
import {ConverterRegistryService} from './converterregistry.service';
import {Converter} from './converter/converter';
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 = { beforeEach(async () => {
getFirst: () => { await TestBed.configureTestingModule({
return firstStep; imports: [AppComponent]
}
};
const converterRegistryServiceStub = {
getAllConverters: (): Converter[] => {
return [];
},
getConverter: (id: string): Converter => {
return undefined;
}
};
beforeEach(async(() => {
/*return */
TestBed.configureTestingModule({
declarations: [AppComponent],
imports: [FormsModule],
providers: [
{provide: InputComponentManagerService, useValue: inputComponentManagerServiceStub},
{provide: ConverterRegistryService, useValue: converterRegistryServiceStub}
]
}) })
.compileComponents(); .compileComponents();
})); });
beforeEach(() => { beforeEach(() => {
fixture = TestBed.createComponent(AppComponent); fixture = TestBed.createComponent(AppComponent);
sut = fixture.componentInstance; sut = fixture.componentInstance;
}); });
it('should create the app', async(() => { it('should create the app', () => {
// const fixture = TestBed.createComponent(AppComponent); // const fixture = TestBed.createComponent(AppComponent);
const app = fixture.debugElement.componentInstance; expect(sut).toBeTruthy();
expect(app).toBeTruthy(); });
}));
}); });

View file

@ -1,57 +1,32 @@
import {Component, OnInit} from '@angular/core'; import {Component, OnInit} from '@angular/core';
import {ConverterRegistryService} from './converterregistry.service'; import {InputComponentManagerService} from './input-component-manager.service';
import {InputComponentManagerService} from './inputcomponentmanager.service';
import {Step} from './step'; import {Step} from './step';
import {Converter} from './converter/converter'; import {RouterOutlet} from '@angular/router';
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';
import {VersionComponent} from './version/version.component';
@Component({ @Component({
selector: 'app-root', selector: 'app-root',
standalone: true,
imports: [
ConverterSelectorComponent,
ErrorMessageComponent,
RouterOutlet,
TextInputFieldComponent,
VersionComponent
],
templateUrl: './app.component.html', templateUrl: './app.component.html',
styleUrls: ['./app.component.scss'] styleUrl: './app.component.scss'
}) })
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();
} }

8
src/app/app.config.ts Normal file
View file

@ -0,0 +1,8 @@
import { ApplicationConfig } from '@angular/core';
import { provideRouter } from '@angular/router';
import { routes } from './app.routes';
export const appConfig: ApplicationConfig = {
providers: [provideRouter(routes)]
};

View file

@ -1,21 +0,0 @@
import {BrowserModule} from '@angular/platform-browser';
import {NgModule} from '@angular/core';
import {AppComponent} from './app.component';
import {ConverterRegistryService} from './converterregistry.service';
import {InputComponentManagerService} from './inputcomponentmanager.service';
import {NativeLibraryWrapperService} from './nativelibrarywrapper.service';
import {FormsModule} from '@angular/forms';
@NgModule({
declarations: [
AppComponent
],
imports: [
BrowserModule,
FormsModule
],
providers: [ConverterRegistryService, InputComponentManagerService, NativeLibraryWrapperService],
bootstrap: [AppComponent]
})
export class AppModule {
}

3
src/app/app.routes.ts Normal file
View file

@ -0,0 +1,3 @@
import { Routes } from '@angular/router';
export const routes: Routes = [];

View file

@ -0,0 +1,40 @@
import {inject, TestBed} from '@angular/core/testing';
import {ConverterRegistryService} from './converter-registry.service';
import {NativeLibraryWrapperService} from './native-library-wrapper.service';
import {Converter} from './converter/converter';
import {Base64Decoder} from './converter/base64-decoder';
import createSpyObj = jasmine.createSpyObj;
describe('ConverterRegistryService', () => {
beforeEach(() => {
TestBed.configureTestingModule({
providers: [
ConverterRegistryService,
{provide: NativeLibraryWrapperService, useValue: createSpyObj(['punycode', 'quotedPrintable', 'utf8'])}
]
});
});
it('should be created', inject([ConverterRegistryService], (service: ConverterRegistryService) => {
expect(service).toBeTruthy();
}));
it('should register converters upon creation', inject([ConverterRegistryService], (service: ConverterRegistryService) => {
expect(service.getAllConverters()).toBeTruthy();
expect(service.getAllConverters().length).toBeGreaterThan(0);
}));
it('must not allow the same converter ID to be regisgered more than once',
inject([ConverterRegistryService], (service: ConverterRegistryService) => {
// arrange
const duplicateConverter: Converter = new Base64Decoder();
const duplicateConverterId = duplicateConverter.getId();
expect(() => {
// act
(service as any).registerConverter(duplicateConverter);
})
// assert
.toThrowError(`Converter-ID ${duplicateConverterId} is already registered!`);
}));
});

View file

@ -1,27 +1,29 @@
import {Injectable} from '@angular/core'; import {Base64Decoder} from './converter/base64-decoder';
import {Base64Encoder} from './converter/base64-encoder';
import {BinToDecConverter} from './converter/bin-to-dec-converter';
import {Converter} from './converter/converter'; import {Converter} from './converter/converter';
import {Base64Encoder} from './converter/base64encoder'; import {DecToBinConverter} from './converter/dec-to-bin-converter';
import {Base64Decoder} from './converter/base64decoder'; import {DecToHexConverter} from './converter/dec-to-hex-converter';
import {URIEncoder} from './converter/uriencoder'; import {HexToDecConverter} from './converter/hex-to-dec-converter';
import {HTMLEntitiesDecoder} from './converter/htmlentities-decoder';
import {HTMLEntitiesEncoder} from './converter/htmlentities-encoder';
import {Injectable} from '@angular/core';
import {NativeLibraryWrapperService} from './native-library-wrapper.service';
import {PunycodeDecoder} from './converter/punycode-decoder';
import {PunycodeEncoder} from './converter/punycode-encoder';
import {QuotedPrintableDecoder} from './converter/quoted-printable-decoder';
import {QuotedPrintableEncoder} from './converter/quoted-printable-encoder';
import {ROT13Converter} from './converter/rot13-converter';
import {URIComponentDecoder} from './converter/uricomponent-decoder';
import {URIComponentEncoder} from './converter/uricomponent-encoder';
import {URIDecoder} from './converter/uridecoder'; import {URIDecoder} from './converter/uridecoder';
import {URIComponentEncoder} from './converter/uricomponentencoder'; import {URIEncoder} from './converter/uriencoder';
import {URIComponentDecoder} from './converter/uricomponentdecoder'; import {UTF8Decoder} from './converter/utf8-decoder';
import {HTMLEntitiesEncoder} from './converter/htmlentitiesencoder'; import {UTF8Encoder} from './converter/utf8-encoder';
import {HTMLEntitiesDecoder} from './converter/htmlentitiesdecoder';
import {DecToHexConverter} from './converter/dectohexconverter';
import {HexToDecConverter} from './converter/hextodecconverter';
import {DecToBinConverter} from './converter/dectobinconverter';
import {BinToDecConverter} from './converter/bintodecconverter';
import {QuotedPrintableDecoder} from './converter/quotedprintabledecoder';
import {QuotedPrintableEncoder} from './converter/quotedprintableencoder';
import {NativeLibraryWrapperService} from './nativelibrarywrapper.service';
import {PunycodeEncoder} from './converter/punycodeencoder';
import {PunycodeDecoder} from './converter/punycodedecoder';
import {UTF8Encoder} from './converter/utf8encoder';
import {UTF8Decoder} from './converter/utf8decoder';
import {ROT13Converter} from './converter/rot13converter';
@Injectable() @Injectable({
providedIn: 'root'
})
export class ConverterRegistryService { export class ConverterRegistryService {
private converters: Converter[] = []; private converters: Converter[] = [];
@ -33,13 +35,8 @@ export class ConverterRegistryService {
return this.converters; return this.converters;
} }
public getConverter(id: string): Converter { public getConverter(id: string): Converter | undefined {
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 {
@ -65,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);
} }
} }

View file

@ -0,0 +1,10 @@
<div [ngClass]="{selectwrapper: true, error: step.error}">
<div class="arrow_box">
<select class="select" (change)="convert($event)">
<option id="undefined">Select conversion ...</option>
@for (c of converters; track c.getId()) {
<option class="option" 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 {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 () => {
await TestBed.configureTestingModule({
imports: [ConverterSelectorComponent],
declarations: [
TestHostComponent
],
providers: [
{provide: InputComponentManagerService, useValue: inputComponentManagerServiceStub},
{provide: ConverterRegistryService, useValue: converterRegistryServiceStub}
]
})
.compileComponents();
});
beforeEach(() => {
testHostFixture = TestBed.createComponent(TestHostComponent);
testHostComponent = testHostFixture.componentInstance;
testHostFixture.detectChanges();
});
it('should create the component', () => {
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,33 @@
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';
import {CommonModule} from '@angular/common';
@Component({
selector: 'app-converter-selector',
templateUrl: './converter-selector.component.html',
standalone: true,
styleUrls: ['./converter-selector.component.scss'],
imports: [CommonModule]
})
export class ConverterSelectorComponent implements OnInit {
@Input()
public step!: Step;
@Input()
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,27 @@
import {Base64Decoder} from './base64-decoder';
describe('Base64Decoder', () => {
let sut: Base64Decoder;
beforeEach(() => sut = new Base64Decoder());
it('should create an instance', () => {
expect(sut).toBeTruthy();
});
it('should have a display name', () => {
expect(sut.getDisplayname()).toBeTruthy();
});
it('should have the id "base64decode"', () => {
expect(sut.getId()).toEqual('base64decode');
});
it('should decode "SGVsbG8sIFdvcmxkIQ==" to "Hello, World!"', () => {
expect(sut.convert('SGVsbG8sIFdvcmxkIQ==')).toEqual('Hello, World!');
});
it('should raise an exception on invalid input', () => {
expect(() => sut.convert('foo bar.')).toThrowError('Could not decode base64 string. Maybe corrupt input?');
});
});

View file

@ -0,0 +1,27 @@
import {Base64Encoder} from './base64-encoder';
describe('Base64Encoder', () => {
let sut: Base64Encoder;
beforeEach(() => sut = new Base64Encoder());
it('should create an instance', () => {
expect(sut).toBeTruthy();
});
it('should have a display name', () => {
expect(sut.getDisplayname()).toBeTruthy();
});
it('should have the id "base64encode"', () => {
expect(sut.getId()).toEqual('base64encode');
});
it('should encode "Oh, guete Tag!" to "T2gsIGd1ZXRlIFRhZyE="', () => {
expect(sut.convert('Oh, guete Tag!')).toEqual('T2gsIGd1ZXRlIFRhZyE=');
});
it('should raise an exception on invalid input', () => {
expect(() => sut.convert('€')).toThrowError(/Looks like you've got a character outside of the Latin1 range there./);
});
});

View file

@ -14,7 +14,7 @@ export class Base64Encoder implements Converter {
return btoa(input); return btoa(input);
} catch (exception) { } catch (exception) {
console.error(exception); console.error(exception);
throw new Error('Ouch! Looks like you\'ve got a UTF-8 character there. Too bad, this is not supported yet. ' throw new Error('Ouch! Looks like you\'ve got a character outside of the Latin1 range there. Too bad, this is not supported yet. '
+ 'We\'re working on it and hope to be ready soon! Why don\'t you ' + 'We\'re working on it and hope to be ready soon! Why don\'t you '
+ '<a href="https://duckduckgo.com/?q=cute+kitties&iar=images">enjoy some kittens</a> meanwhile?'); + '<a href="https://duckduckgo.com/?q=cute+kitties&iar=images">enjoy some kittens</a> meanwhile?');
} }

View file

@ -0,0 +1,27 @@
import {BinToDecConverter} from './bin-to-dec-converter';
describe('BinToDecConverter', () => {
let sut: BinToDecConverter;
beforeEach(() => sut = new BinToDecConverter());
it('should create an instance', () => {
expect(sut).toBeTruthy();
});
it('should have a display name', () => {
expect(sut.getDisplayname()).toBeTruthy();
});
it('should have the id "bintodec"', () => {
expect(sut.getId()).toEqual('bintodec');
});
it('should convert "11011" to "27"', () => {
expect(sut.convert('11011')).toEqual('27');
});
it('should raise an exception on invalid input', () => {
expect(() => sut.convert('1foo bar')).toThrowError('The input seems not to be a valid binary number.');
});
});

View file

@ -11,7 +11,7 @@ export class BinToDecConverter implements Converter {
convert(input: string): string { convert(input: string): string {
const n: number = parseInt(input, 2); const n: number = parseInt(input, 2);
if (isNaN(n)) { if (isNaN(n) || !input.trim().match(/^([01]+)$/)) {
throw new Error('The input seems not to be a valid binary number.'); throw new Error('The input seems not to be a valid binary number.');
} }
return n.toString(10); return n.toString(10);

View file

@ -0,0 +1,27 @@
import {DecToBinConverter} from './dec-to-bin-converter';
describe('DecToBinConverter', () => {
let sut: DecToBinConverter;
beforeEach(() => sut = new DecToBinConverter());
it('should create an instance', () => {
expect(sut).toBeTruthy();
});
it('should have a display name', () => {
expect(sut.getDisplayname()).toBeTruthy();
});
it('should have the id "dectobin"', () => {
expect(sut.getId()).toEqual('dectobin');
});
it('should convert "22" to "10110"', () => {
expect(sut.convert('22')).toEqual('10110');
});
it('should raise an exception on invalid input', () => {
expect(() => sut.convert('foo bar')).toThrowError('The input seems not to be a valid integer.');
});
});

View file

@ -0,0 +1,27 @@
import {DecToHexConverter} from './dec-to-hex-converter';
describe('DecToHexConverter', () => {
let sut: DecToHexConverter;
beforeEach(() => sut = new DecToHexConverter());
it('should create an instance', () => {
expect(sut).toBeTruthy();
});
it('should have a display name', () => {
expect(sut.getDisplayname()).toBeTruthy();
});
it('should have the id "dectohex"', () => {
expect(sut.getId()).toEqual('dectohex');
});
it('should convert "22" to "16"', () => {
expect(sut.convert('22')).toEqual('16');
});
it('should raise an exception on invalid input', () => {
expect(() => sut.convert('foo bar')).toThrowError('The input seems not to be a valid integer.');
});
});

View file

@ -0,0 +1,27 @@
import {HexToDecConverter} from './hex-to-dec-converter';
describe('HexToDecConverter', () => {
let sut: HexToDecConverter;
beforeEach(() => sut = new HexToDecConverter());
it('should create an instance', () => {
expect(sut).toBeTruthy();
});
it('should have a display name', () => {
expect(sut.getDisplayname()).toBeTruthy();
});
it('should have the id "hextodec"', () => {
expect(sut.getId()).toEqual('hextodec');
});
it('should convert "ab" to "171"', () => {
expect(sut.convert('ab')).toEqual('171');
});
it('should raise an exception on invalid input', () => {
expect(() => sut.convert('foo bar')).toThrowError('The input seems not to be a valid hexadecimal number.');
});
});

View file

@ -11,7 +11,7 @@ export class HexToDecConverter implements Converter {
convert(input: string): string { convert(input: string): string {
const n: number = parseInt(input, 16); const n: number = parseInt(input, 16);
if (isNaN(n)) { if (isNaN(n) || !input.trim().match(/^((0x|0X)?[0-9a-fA-F]+)$/)) {
throw new Error('The input seems not to be a valid hexadecimal number.'); throw new Error('The input seems not to be a valid hexadecimal number.');
} }
return n.toString(10); return n.toString(10);

View file

@ -0,0 +1,24 @@
import {HTMLEntitiesDecoder} from './htmlentities-decoder';
describe('HTMLEntitiesDecoder', () => {
let sut: HTMLEntitiesDecoder;
beforeEach(() => sut = new HTMLEntitiesDecoder());
it('should create an instance', () => {
expect(sut).toBeTruthy();
});
it('should have a display name', () => {
expect(sut.getDisplayname()).toBeTruthy();
});
it('should have the id "decodehtmlentities"', () => {
expect(sut.getId()).toEqual('decodehtmlentities');
});
it('should decode "&lt;span&gt;&quot;Hi&quot; &amp; &quot;Lo&quot;&lt;/span&gt;" to "<span>"Hi" & "Lo"</span>"', () => {
expect(sut.convert('&lt;span&gt;&quot;Hi&quot; &amp; &quot;Lo&quot;&lt;/span&gt;'))
.toEqual('<span>"Hi" & "Lo"</span>');
});
});

View file

@ -0,0 +1,24 @@
import {HTMLEntitiesEncoder} from './htmlentities-encoder';
describe('HTMLEntitiesEncoder', () => {
let sut: HTMLEntitiesEncoder;
beforeEach(() => sut = new HTMLEntitiesEncoder());
it('should create an instance', () => {
expect(sut).toBeTruthy();
});
it('should have a display name', () => {
expect(sut.getDisplayname()).toBeTruthy();
});
it('should have the id "encodehtmlentities"', () => {
expect(sut.getId()).toEqual('encodehtmlentities');
});
it('should encode "<span>"Hi" & "Lo"</span>" to "&lt;span&gt;&quot;Hi&quot; &amp; &quot;Lo&quot;&lt;/span&gt;"', () => {
expect(sut.convert('<span>"Hi" & "Lo"</span>'))
.toEqual('&lt;span&gt;&quot;Hi&quot; &amp; &quot;Lo&quot;&lt;/span&gt;');
});
});

View file

@ -0,0 +1,37 @@
import {PunycodeDecoder} from './punycode-decoder';
import {NativeLibraryWrapperService, Punycode} from '../native-library-wrapper.service';
import createSpyObj = jasmine.createSpyObj;
import Spy = jasmine.Spy;
describe('PunycodeDecoder', () => {
let sut: PunycodeDecoder;
let nativeWrapperSpy: Partial<NativeLibraryWrapperService>;
let decodeSpy: Spy;
beforeEach(() => {
nativeWrapperSpy = {punycode: createSpyObj<Punycode>(['decode'])};
decodeSpy = nativeWrapperSpy.punycode!.decode as Spy;
sut = new PunycodeDecoder((nativeWrapperSpy as NativeLibraryWrapperService));
});
it('should create an instance', () => {
expect(sut).toBeTruthy();
});
it('should have a display name', () => {
expect(sut.getDisplayname()).toBeTruthy();
});
it('should have the id "decodepunycode"', () => {
expect(sut.getId()).toEqual('decodepunycode');
});
it('should call through to the native punycode decoder', () => {
const testInput = 'My input';
const expectedOutput = 'It worked';
decodeSpy.and.returnValue(expectedOutput);
const result: string = sut.convert(testInput);
expect(result).toEqual(expectedOutput);
expect(decodeSpy).toHaveBeenCalledWith(testInput);
});
});

View file

@ -1,5 +1,5 @@
import {Converter} from './converter'; import {Converter} from './converter';
import {NativeLibraryWrapperService} from '../nativelibrarywrapper.service'; import {NativeLibraryWrapperService} from '../native-library-wrapper.service';
export class PunycodeDecoder implements Converter { export class PunycodeDecoder implements Converter {

View file

@ -0,0 +1,37 @@
import {NativeLibraryWrapperService, Punycode} from '../native-library-wrapper.service';
import {PunycodeEncoder} from './punycode-encoder';
import createSpyObj = jasmine.createSpyObj;
import Spy = jasmine.Spy;
describe('PunycodeEncoder', () => {
let sut: PunycodeEncoder;
let nativeWrapperSpy: Partial<NativeLibraryWrapperService>;
let encodeSpy: Spy;
beforeEach(() => {
nativeWrapperSpy = {punycode: createSpyObj<Punycode>(['encode'])};
encodeSpy = nativeWrapperSpy.punycode!.encode as Spy;
sut = new PunycodeEncoder(nativeWrapperSpy as NativeLibraryWrapperService);
});
it('should create an instance', () => {
expect(sut).toBeTruthy();
});
it('should have a display name', () => {
expect(sut.getDisplayname()).toBeTruthy();
});
it('should have the id "encodepunycode"', () => {
expect(sut.getId()).toEqual('encodepunycode');
});
it('should call through to the native punycode encoder', () => {
const testInput = 'My input';
const expectedOutput = 'It worked';
encodeSpy.and.returnValue(expectedOutput);
const result: string = sut.convert(testInput);
expect(result).toEqual(expectedOutput);
expect(encodeSpy).toHaveBeenCalledWith(testInput);
});
});

View file

@ -1,5 +1,5 @@
import {Converter} from './converter'; import {Converter} from './converter';
import {NativeLibraryWrapperService} from '../nativelibrarywrapper.service'; import {NativeLibraryWrapperService} from '../native-library-wrapper.service';
export class PunycodeEncoder implements Converter { export class PunycodeEncoder implements Converter {

View file

@ -0,0 +1,37 @@
import {NativeLibraryWrapperService, QuotedPrintable} from '../native-library-wrapper.service';
import {QuotedPrintableDecoder} from './quoted-printable-decoder';
import createSpyObj = jasmine.createSpyObj;
import Spy = jasmine.Spy;
describe('QuotedPrintableDecoder', () => {
let sut: QuotedPrintableDecoder;
let nativeWrapperSpy: Partial<NativeLibraryWrapperService>;
let decodeSpy: Spy;
beforeEach(() => {
nativeWrapperSpy = {quotedPrintable: createSpyObj<QuotedPrintable>(['decode'])};
decodeSpy = nativeWrapperSpy.quotedPrintable!.decode as Spy;
sut = new QuotedPrintableDecoder((nativeWrapperSpy as NativeLibraryWrapperService));
});
it('should create an instance', () => {
expect(sut).toBeTruthy();
});
it('should have a display name', () => {
expect(sut.getDisplayname()).toBeTruthy();
});
it('should have the id "decodequotedprintable"', () => {
expect(sut.getId()).toEqual('decodequotedprintable');
});
it('should call through to the native quoted printable decoder', () => {
const testInput = 'My input';
const expectedOutput = 'It worked';
decodeSpy.and.returnValue(expectedOutput);
const result: string = sut.convert(testInput);
expect(result).toEqual(expectedOutput);
expect(decodeSpy).toHaveBeenCalledWith(testInput);
});
});

View file

@ -1,5 +1,5 @@
import {Converter} from './converter'; import {Converter} from './converter';
import {NativeLibraryWrapperService} from '../nativelibrarywrapper.service'; import {NativeLibraryWrapperService} from '../native-library-wrapper.service';
export class QuotedPrintableDecoder implements Converter { export class QuotedPrintableDecoder implements Converter {

View file

@ -0,0 +1,37 @@
import {NativeLibraryWrapperService, QuotedPrintable} from '../native-library-wrapper.service';
import {QuotedPrintableEncoder} from './quoted-printable-encoder';
import createSpyObj = jasmine.createSpyObj;
import Spy = jasmine.Spy;
describe('QuotedPrintableEncoder', () => {
let sut: QuotedPrintableEncoder;
let nativeWrapperSpy: Partial<NativeLibraryWrapperService>;
let encodeSpy: Spy;
beforeEach(() => {
nativeWrapperSpy = {quotedPrintable: createSpyObj<QuotedPrintable>(['encode'])};
encodeSpy = nativeWrapperSpy.quotedPrintable!.encode as Spy;
sut = new QuotedPrintableEncoder(nativeWrapperSpy as NativeLibraryWrapperService);
});
it('should create an instance', () => {
expect(sut).toBeTruthy();
});
it('should have a display name', () => {
expect(sut.getDisplayname()).toBeTruthy();
});
it('should have the id "encodequotedprintable"', () => {
expect(sut.getId()).toEqual('encodequotedprintable');
});
it('should call through to the native quoted printable encoder', () => {
const testInput = 'My input';
const expectedOutput = 'It worked';
encodeSpy.and.returnValue(expectedOutput);
const result: string = sut.convert(testInput);
expect(result).toEqual(expectedOutput);
expect(encodeSpy).toHaveBeenCalledWith(testInput);
});
});

View file

@ -1,5 +1,5 @@
import {Converter} from './converter'; import {Converter} from './converter';
import {NativeLibraryWrapperService} from '../nativelibrarywrapper.service'; import {NativeLibraryWrapperService} from '../native-library-wrapper.service';
export class QuotedPrintableEncoder implements Converter { export class QuotedPrintableEncoder implements Converter {

View file

@ -0,0 +1,28 @@
import {ROT13Converter} from './rot13-converter';
describe('ROT13Converter', () => {
let sut: ROT13Converter;
beforeEach(() => sut = new ROT13Converter());
it('should create an instance', () => {
expect(sut).toBeTruthy();
});
it('should have a display name', () => {
expect(sut.getDisplayname()).toBeTruthy();
});
it('should have the id "rot13"', () => {
expect(sut.getId()).toEqual('rot13');
});
it('should encode "Hello, World!" to "Uryyb, Jbeyq!"', () => {
expect(sut.convert('Hello, World!')).toEqual('Uryyb, Jbeyq!');
});
it('should return the original input after being applied twice', () => {
const input = 'Ok, so this string is just a bunch of letters. And numbers: 1, 2, 3. Ans others: /&%. Kthxbye!';
expect(sut.convert(sut.convert(input))).toEqual(input);
});
});

View file

@ -6,14 +6,14 @@ export class ROT13Converter implements Converter {
} }
getId(): string { getId(): string {
return 'rot13convert'; return 'rot13';
} }
convert(input: string): string { convert(input: string): string {
try { try {
const inChars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'; const inChars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
const outChars = 'NOPQRSTUVWXYZABCDEFGHIJKLMnopqrstuvwxyzabcdefghijklm'; const outChars = 'NOPQRSTUVWXYZABCDEFGHIJKLMnopqrstuvwxyzabcdefghijklm';
const translate = c => { const translate = (c: string) => {
const charIndex = inChars.indexOf(c); const charIndex = inChars.indexOf(c);
return charIndex > -1 ? outChars[charIndex] : c; return charIndex > -1 ? outChars[charIndex] : c;
}; };

View file

@ -0,0 +1,25 @@
import {URIComponentDecoder} from './uricomponent-decoder';
describe('URIComponentDecoder', () => {
let sut: URIComponentDecoder;
beforeEach(() => sut = new URIComponentDecoder());
it('should create an instance', () => {
expect(sut).toBeTruthy();
});
it('should have a display name', () => {
expect(sut.getDisplayname()).toBeTruthy();
});
it('should have the id "uricomponentdecode"', () => {
expect(sut.getId()).toEqual('uricomponentdecode');
});
it('should decode "http%3A%2F%2Fm%C3%A4nu%3Agh%C3%ABim%40host%3Aport%2Fhi.there%3Foh%3Dwell%23ya" ' +
'to "http://mänu:ghëim@host:port/hi.there?oh=well#ya"', () => {
expect(sut.convert('http%3A%2F%2Fm%C3%A4nu%3Agh%C3%ABim%40host%3Aport%2Fhi.there%3Foh%3Dwell%23ya'))
.toEqual('http://mänu:ghëim@host:port/hi.there?oh=well#ya');
});
});

View file

@ -0,0 +1,25 @@
import {URIComponentEncoder} from './uricomponent-encoder';
describe('URIComponentEncoder', () => {
let sut: URIComponentEncoder;
beforeEach(() => sut = new URIComponentEncoder());
it('should create an instance', () => {
expect(sut).toBeTruthy();
});
it('should have a display name', () => {
expect(sut.getDisplayname()).toBeTruthy();
});
it('should have the id "uricomponentencode"', () => {
expect(sut.getId()).toEqual('uricomponentencode');
});
it('should encode "http://mänu:ghëim@host:port/hi.there?oh=well#ya" ' +
'to "http%3A%2F%2Fm%C3%A4nu%3Agh%C3%ABim%40host%3Aport%2Fhi.there%3Foh%3Dwell%23ya"', () => {
expect(sut.convert('http://mänu:ghëim@host:port/hi.there?oh=well#ya'))
.toEqual('http%3A%2F%2Fm%C3%A4nu%3Agh%C3%ABim%40host%3Aport%2Fhi.there%3Foh%3Dwell%23ya');
});
});

View file

@ -0,0 +1,25 @@
import {URIDecoder} from './uridecoder';
describe('URIDecoder', () => {
let sut: URIDecoder;
beforeEach(() => sut = new URIDecoder());
it('should create an instance', () => {
expect(sut).toBeTruthy();
});
it('should have a display name', () => {
expect(sut.getDisplayname()).toBeTruthy();
});
it('should have the id "uridecode"', () => {
expect(sut.getId()).toEqual('uridecode');
});
it('should decode "http://m%C3%A4nu:gh%C3%ABim@host:port/hi.there?oh=well#ya" ' +
'to "http://mänu:ghëim@host:port/hi.there?oh=well#ya"', () => {
expect(sut.convert('http://m%C3%A4nu:gh%C3%ABim@host:port/hi.there?oh=well#ya'))
.toEqual('http://mänu:ghëim@host:port/hi.there?oh=well#ya');
});
});

View file

@ -0,0 +1,25 @@
import {URIEncoder} from './uriencoder';
describe('URIEncoder', () => {
let sut: URIEncoder;
beforeEach(() => sut = new URIEncoder());
it('should create an instance', () => {
expect(sut).toBeTruthy();
});
it('should have a display name', () => {
expect(sut.getDisplayname()).toBeTruthy();
});
it('should have the id "uriencode"', () => {
expect(sut.getId()).toEqual('uriencode');
});
it('should encode "http://mänu:ghëim@host:port/hi.there?oh=well#ya" ' +
'to "http://m%C3%A4nu:gh%C3%ABim@host:port/hi.there?oh=well#ya"', () => {
expect(sut.convert('http://mänu:ghëim@host:port/hi.there?oh=well#ya'))
.toEqual('http://m%C3%A4nu:gh%C3%ABim@host:port/hi.there?oh=well#ya');
});
});

View file

@ -0,0 +1,37 @@
import {NativeLibraryWrapperService, Utf8} from '../native-library-wrapper.service';
import {UTF8Decoder} from './utf8-decoder';
import createSpyObj = jasmine.createSpyObj;
import Spy = jasmine.Spy;
describe('UTF8Decoder', () => {
let sut: UTF8Decoder;
let nativeWrapperSpy: Partial<NativeLibraryWrapperService>;
let decodeSpy: Spy;
beforeEach(() => {
nativeWrapperSpy = {utf8: createSpyObj<Utf8>(['decode'])};
decodeSpy = nativeWrapperSpy.utf8!.decode as Spy;
sut = new UTF8Decoder((nativeWrapperSpy as NativeLibraryWrapperService));
});
it('should create an instance', () => {
expect(sut).toBeTruthy();
});
it('should have a display name', () => {
expect(sut.getDisplayname()).toBeTruthy();
});
it('should have the id "decodeutf8"', () => {
expect(sut.getId()).toEqual('decodeutf8');
});
it('should call through to the native UTF-8 decoder', () => {
const testInput = 'My input';
const expectedOutput = 'It worked';
decodeSpy.and.returnValue(expectedOutput);
const result: string = sut.convert(testInput);
expect(result).toEqual(expectedOutput);
expect(decodeSpy).toHaveBeenCalledWith(testInput);
});
});

View file

@ -1,5 +1,5 @@
import {Converter} from './converter'; import {Converter} from './converter';
import {NativeLibraryWrapperService} from '../nativelibrarywrapper.service'; import {NativeLibraryWrapperService} from '../native-library-wrapper.service';
export class UTF8Decoder implements Converter { export class UTF8Decoder implements Converter {

View file

@ -0,0 +1,37 @@
import {NativeLibraryWrapperService, Utf8} from '../native-library-wrapper.service';
import {UTF8Encoder} from './utf8-encoder';
import createSpyObj = jasmine.createSpyObj;
import Spy = jasmine.Spy;
describe('UTF8Encoder', () => {
let sut: UTF8Encoder;
let nativeWrapperSpy: Partial<NativeLibraryWrapperService>;
let encodeSpy: Spy;
beforeEach(() => {
nativeWrapperSpy = {utf8: createSpyObj<Utf8>(['encode'])};
encodeSpy = nativeWrapperSpy.utf8!.encode as Spy;
sut = new UTF8Encoder(nativeWrapperSpy as NativeLibraryWrapperService);
});
it('should create an instance', () => {
expect(sut).toBeTruthy();
});
it('should have a display name', () => {
expect(sut.getDisplayname()).toBeTruthy();
});
it('should have the id "encodeutf8"', () => {
expect(sut.getId()).toEqual('encodeutf8');
});
it('should call through to the native UTF-8 encoder', () => {
const testInput = 'My input';
const expectedOutput = 'It worked';
encodeSpy.and.returnValue(expectedOutput);
const result: string = sut.convert(testInput);
expect(result).toEqual(expectedOutput);
expect(encodeSpy).toHaveBeenCalledWith(testInput);
});
});

View file

@ -1,5 +1,5 @@
import {Converter} from './converter'; import {Converter} from './converter';
import {NativeLibraryWrapperService} from '../nativelibrarywrapper.service'; import {NativeLibraryWrapperService} from '../native-library-wrapper.service';
export class UTF8Encoder implements Converter { export class UTF8Encoder implements Converter {

View file

@ -0,0 +1,3 @@
@if (step.error) {
<div class="errormessage" [innerHTML]="step.message"></div>
}

View file

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

View file

@ -0,0 +1,50 @@
import {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 () => {
await TestBed.configureTestingModule({
imports: [ErrorMessageComponent],
declarations: [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,16 @@
import {Component, Input} from '@angular/core';
import {Step} from '../step';
@Component({
selector: 'app-error-message',
templateUrl: './error-message.component.html',
standalone: true,
styleUrls: ['./error-message.component.scss']
})
export class ErrorMessageComponent {
@Input()
public step!: Step;
constructor() {
}
}

View file

@ -0,0 +1,71 @@
import {inject, TestBed} from '@angular/core/testing';
import {InputComponentManagerService} from './input-component-manager.service';
import {Step} from './step';
describe('InputComponentManagerService', () => {
beforeEach(() => {
TestBed.configureTestingModule({
providers: [InputComponentManagerService]
});
});
it('should be created', inject([InputComponentManagerService], (service: InputComponentManagerService) => {
expect(service).toBeTruthy();
}));
it('should create a component if requesting the first one',
inject([InputComponentManagerService], (service: InputComponentManagerService) => {
const firstStep: Step = service.getFirst();
expect(firstStep).toBeTruthy();
expect(service.getFirst()).toBe(firstStep);
expect(service.getAllComponents().length).toEqual(1);
})
);
it('must not add a null Step', inject([InputComponentManagerService], (service: InputComponentManagerService) => {
expect(() => service.register(null!)).toThrowError();
expect(service.getAllComponents().length).toEqual(0);
}));
it('must not add an undefined Step', inject([InputComponentManagerService], (service: InputComponentManagerService) => {
expect(() => service.register(undefined!)).toThrowError();
expect(service.getAllComponents().length).toEqual(0);
}));
it('should register new Steps', inject([InputComponentManagerService], (service: InputComponentManagerService) => {
service.register(new Step(99));
expect(service.getAllComponents().length).toEqual(1);
service.register(new Step(100));
expect(service.getAllComponents().length).toEqual(2);
}));
it('should return a new step if the next step doesn\'t exist',
inject([InputComponentManagerService], (service: InputComponentManagerService) => {
// arrange
const firstStep: Step = new Step(0);
service.register(firstStep);
// act
const nextStep: Step = service.getNext(firstStep);
// assert
expect(service.getAllComponents().length).toEqual(2);
expect(nextStep.index).toEqual(1);
})
);
it('should return the next step if requested', inject([InputComponentManagerService], (service: InputComponentManagerService) => {
// arrange
const firstStep: Step = new Step(0);
const nextStep: Step = new Step(1);
service.register(firstStep);
service.register(nextStep);
// act
const requestedStep: Step = service.getNext(firstStep);
// assert
expect(service.getAllComponents().length).toEqual(2);
expect(requestedStep).toEqual(nextStep);
}));
});

View file

@ -1,7 +1,9 @@
import {Injectable} from '@angular/core'; import {Injectable} from '@angular/core';
import {Step} from './step'; import {Step} from './step';
@Injectable() @Injectable({
providedIn: 'root'
})
export class InputComponentManagerService { export class InputComponentManagerService {
private components: Step[] = []; private components: Step[] = [];
@ -9,6 +11,9 @@ export class InputComponentManagerService {
} }
public register(component: Step): void { public register(component: Step): void {
if (!component) {
throw new Error('component to add must not be empty or undefined');
}
this.components.push(component); this.components.push(component);
} }

View file

@ -0,0 +1,33 @@
import {inject, TestBed} from '@angular/core/testing';
import {NativeLibraryWrapperService} from './native-library-wrapper.service';
describe('NativeLibraryWrapperService', () => {
beforeEach(() => {
TestBed.configureTestingModule({
providers: [NativeLibraryWrapperService]
});
});
it('should be created', inject([NativeLibraryWrapperService], (service: NativeLibraryWrapperService) => {
expect(service).toBeTruthy();
}));
it('should convert punycode', inject([NativeLibraryWrapperService], (service: NativeLibraryWrapperService) => {
expect(service.punycode).toBeTruthy();
expect(service.punycode.encode('bärneruhr')).toEqual('brneruhr-0za');
expect(service.punycode.decode('brneruhr-0za')).toEqual('bärneruhr');
}));
it('should convert utf8', inject([NativeLibraryWrapperService], (service: NativeLibraryWrapperService) => {
expect(service.utf8).toBeTruthy();
expect(service.utf8.encode('bärneruhr')).toEqual('bärneruhr');
expect(service.utf8.decode('bärneruhr')).toEqual('bärneruhr');
}));
it('should convert quoted printable', inject([NativeLibraryWrapperService], (service: NativeLibraryWrapperService) => {
expect(service.quotedPrintable).toBeTruthy();
expect(service.quotedPrintable.encode('bärneruhr')).toEqual('b=E4rneruhr');
expect(service.quotedPrintable.decode('b=E4rneruhr')).toEqual('bärneruhr');
}));
});

View file

@ -0,0 +1,37 @@
import {Injectable} from '@angular/core';
import * as NativePunycode from 'punycode/';
import * as NativeQuotedPrintable from 'quoted-printable';
import * as NativeUtf8 from 'utf8';
@Injectable({
providedIn: 'root'
})
export class NativeLibraryWrapperService {
public readonly utf8: Utf8;
public readonly quotedPrintable: QuotedPrintable;
public readonly punycode: Punycode;
constructor() {
this.utf8 = NativeUtf8;
this.quotedPrintable = NativeQuotedPrintable;
this.punycode = NativePunycode;
}
}
export interface Punycode {
encode(input: string): string;
decode(input: string): string;
}
export interface QuotedPrintable {
encode(input: string): string;
decode(input: string): string;
}
export interface Utf8 {
encode(input: any): string;
decode(input: string): any;
}

View file

@ -1,20 +0,0 @@
import {Injectable} from '@angular/core';
import {Punycode} from './punycode';
import {Utf8} from './utf8';
import {QuotedPrintable} from './quotedprintable';
import * as NativeUtf8 from 'utf8';
import * as NativeQuotedPrintable from 'quoted-printable';
import * as NativePunycode from 'punycode';
@Injectable()
export class NativeLibraryWrapperService {
public utf8: Utf8;
public quotedPrintable: QuotedPrintable;
public punycode: Punycode;
constructor() {
this.utf8 = NativeUtf8;
this.quotedPrintable = NativeQuotedPrintable;
this.punycode = NativePunycode;
}
}

View file

@ -1,4 +0,0 @@
export interface Punycode {
encode(input: string): string;
decode(input: string): string;
}

View file

@ -1,4 +0,0 @@
export interface QuotedPrintable {
encode(input: string): string;
decode(input: string): string;
}

View file

@ -2,7 +2,7 @@ import {Converter} from './converter/converter';
export class Step { export class Step {
public content = ''; public content = '';
public selectedConverter: Converter = undefined; public selectedConverter: Converter | undefined;
public index: number; public index: number;
public error = false; public error = false;
public message = ''; public message = '';

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,109 @@
import {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 () => {
await TestBed.configureTestingModule({
declarations: [
TestHostComponent
],
imports: [TextInputFieldComponent, FormsModule],
providers: [{provide: InputComponentManagerService, useValue: inputComponentManagerServiceStub}]
})
.compileComponents();
});
beforeEach(() => {
testHostFixture = TestBed.createComponent(TestHostComponent);
testHostComponent = testHostFixture.componentInstance;
testHostFixture.detectChanges();
});
it('should create the component', () => {
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,48 @@
import {Component, Input} from '@angular/core';
import {Step} from '../step';
import {Converter} from '../converter/converter';
import {InputComponentManagerService} from '../input-component-manager.service';
import {FormsModule} from '@angular/forms';
@Component({
selector: 'app-text-input-field',
templateUrl: './text-input-field.component.html',
standalone: true,
styleUrls: ['./text-input-field.component.scss'],
imports: [FormsModule]
})
export class TextInputFieldComponent {
@Input()
public step!: Step;
constructor(private inputComponentManagerService: InputComponentManagerService) {
}
update(step: Step): void {
const converter: Converter | undefined = step.selectedConverter;
if (converter !== undefined) {
const content: string = step.content;
let result: string | null;
try {
result = converter.convert(content);
} catch (error) {
if (typeof console === 'object' && typeof console.log === 'function') {
console.log(error);
}
step.message = (error as 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);
}
}
}
}
}

View file

@ -1,4 +0,0 @@
export interface Utf8 {
encode(input: any): string;
decode(input: string): any;
}

View file

@ -0,0 +1 @@
<div [ngClass]="{dev: !PROD}">Version: {{VERSION}}</div>

View file

@ -0,0 +1,10 @@
div {
font-size: smaller;
color: gray;
display: block;
text-align: right;
margin-right: 2em;
&.dev {
color: red;
}
}

View file

@ -0,0 +1,36 @@
import {ComponentFixture, TestBed} from '@angular/core/testing';
import {VersionComponent} from './version.component';
import packageInfo from '../../../package.json';
describe('VersionComponent', () => {
let component: VersionComponent;
let fixture: ComponentFixture<VersionComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [VersionComponent]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(VersionComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create the component', () => {
expect(component).toBeTruthy();
});
it('should contain the correct version', () => {
// when executing the test, we're always running with the dev environment
expect(component.VERSION).toEqual(`${packageInfo.version} (development build)`);
});
it('should have the correct value for the "production" property', () => {
// when executing the test, we're always running with the dev environment
expect(component.PROD).toBeFalsy();
});
});

View file

@ -0,0 +1,18 @@
import {Component, isDevMode} from '@angular/core';
import {CommonModule} from '@angular/common';
import packageInfo from '../../../package.json';
@Component({
selector: 'app-version',
templateUrl: './version.component.html',
standalone: true,
styleUrls: ['./version.component.scss'],
imports: [CommonModule]
})
export class VersionComponent {
public readonly PROD: boolean = !isDevMode();
public readonly VERSION: string = packageInfo.version + (this.PROD ? '' : ' (development build)');
constructor() {
}
}

View file

@ -1,11 +0,0 @@
# This file is currently used by autoprefixer to adjust CSS to support the below specified browsers
# For additional information regarding the format and rule options, please see:
# https://github.com/browserslist/browserslist#queries
#
# For IE 9-11 support, please remove 'not' from the last line of the file and adjust as needed
> 0.5%
last 2 versions
Firefox ESR
not dead
not IE 9-11

View file

@ -1,3 +0,0 @@
export const environment = {
production: true
};

View file

@ -1,15 +0,0 @@
// This file can be replaced during build by using the `fileReplacements` array.
// `ng build ---prod` replaces `environment.ts` with `environment.prod.ts`.
// The list of file replacements can be found in `angular.json`.
export const environment = {
production: false
};
/*
* In development mode, for easier debugging, you can ignore zone related error
* stack frames such as `zone.run`/`zoneDelegate.invokeTask` by importing the
* below file. Don't forget to comment it out in production mode
* because it will have a performance impact when errors are thrown
*/
// import 'zone.js/dist/zone-error'; // Included with Angular CLI.

View file

@ -1,31 +0,0 @@
// Karma configuration file, see link for more information
// https://karma-runner.github.io/1.0/config/configuration-file.html
module.exports = function (config) {
config.set({
basePath: '',
frameworks: ['jasmine', '@angular-devkit/build-angular'],
plugins: [
require('karma-jasmine'),
require('karma-chrome-launcher'),
require('karma-jasmine-html-reporter'),
require('karma-coverage-istanbul-reporter'),
require('@angular-devkit/build-angular/plugins/karma')
],
client: {
clearContext: false // leave Jasmine Spec Runner output visible in browser
},
coverageIstanbulReporter: {
dir: require('path').join(__dirname, '../coverage'),
reports: ['html', 'lcovonly'],
fixWebpackSourcePaths: true
},
reporters: ['progress', 'kjhtml'],
port: 9876,
colors: true,
logLevel: config.LOG_INFO,
autoWatch: true,
browsers: ['Chrome'],
singleRun: false
});
};

View file

@ -1,12 +1,6 @@
import { enableProdMode } from '@angular/core'; import { bootstrapApplication } from '@angular/platform-browser';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; import { appConfig } from './app/app.config';
import { AppComponent } from './app/app.component';
import { AppModule } from './app/app.module'; bootstrapApplication(AppComponent, appConfig)
import { environment } from './environments/environment'; .catch((err) => console.error(err));
if (environment.production) {
enableProdMode();
}
platformBrowserDynamic().bootstrapModule(AppModule)
.catch(err => console.log(err));

View file

@ -1,76 +0,0 @@
/**
* This file includes polyfills needed by Angular and is loaded before the app.
* You can add your own extra polyfills to this file.
*
* This file is divided into 2 sections:
* 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers.
* 2. Application imports. Files imported after ZoneJS that should be loaded before your main
* file.
*
* The current setup is for so-called "evergreen" browsers; the last versions of browsers that
* automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera),
* Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile.
*
* Learn more in https://angular.io/docs/ts/latest/guide/browser-support.html
*/
/***************************************************************************************************
* BROWSER POLYFILLS
*/
/** IE9, IE10 and IE11 requires all of the following polyfills. **/
// import 'core-js/es6/symbol';
// import 'core-js/es6/object';
// import 'core-js/es6/function';
// import 'core-js/es6/parse-int';
// import 'core-js/es6/parse-float';
// import 'core-js/es6/number';
// import 'core-js/es6/math';
// import 'core-js/es6/string';
// import 'core-js/es6/date';
// import 'core-js/es6/array';
// import 'core-js/es6/regexp';
// import 'core-js/es6/map';
// import 'core-js/es6/weak-map';
// import 'core-js/es6/set';
/** IE10 and IE11 requires the following for NgClass support on SVG elements */
// import 'classlist.js'; // Run `npm install --save classlist.js`.
/** IE10 and IE11 requires the following for the Reflect API. */
import 'core-js/es6/reflect';
/** Evergreen browsers require these. **/
// Used for reflect-metadata in JIT. If you use AOT (and only Angular decorators), you can remove.
import 'core-js/es7/reflect';
/***************************************************************************************************
* Zone JS is required by default for Angular itself.
*/
import 'zone.js/dist/zone'; // Included with Angular CLI.
/**
* Web Animations `@angular/platform-browser/animations`
* Only required if AnimationBuilder is used within the application and using IE/Edge or Safari.
* Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0).
**/
// import 'web-animations-js'; // Run `npm install --save web-animations-js`.
/**
* By default, zone.js will patch all possible macroTask and DomEvents
* user can disable parts of macroTask/DomEvents patch by setting following flags
*/
// (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame
// (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick
// (window as any).__zone_symbol__BLACK_LISTED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames
/*
* in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js
* with the following flag, it will bypass `zone.js` patch for IE/Edge
*/
// (window as any).__Zone_enable_cross_context_check = true;
/***************************************************************************************************
* APPLICATION IMPORTS
*/

View file

@ -1,20 +0,0 @@
// This file is required by karma.conf.js and loads recursively all the .spec and framework files
import 'zone.js/dist/zone-testing';
import { getTestBed } from '@angular/core/testing';
import {
BrowserDynamicTestingModule,
platformBrowserDynamicTesting
} from '@angular/platform-browser-dynamic/testing';
declare const require: any;
// First, initialize the Angular testing environment.
getTestBed().initTestEnvironment(
BrowserDynamicTestingModule,
platformBrowserDynamicTesting()
);
// Then we find all the tests.
const context = require.context('./', true, /\.spec\.ts$/);
// And load the modules.
context.keys().map(context);

View file

@ -1,11 +0,0 @@
{
"extends": "../tsconfig.json",
"compilerOptions": {
"outDir": "../out-tsc/app",
"types": []
},
"exclude": [
"test.ts",
"**/*.spec.ts"
]
}

View file

@ -1,18 +0,0 @@
{
"extends": "../tsconfig.json",
"compilerOptions": {
"outDir": "../out-tsc/spec",
"types": [
"jasmine",
"node"
]
},
"files": [
"test.ts",
"polyfills.ts"
],
"include": [
"**/*.spec.ts",
"**/*.d.ts"
]
}

View file

@ -1,17 +0,0 @@
{
"extends": "../tslint.json",
"rules": {
"directive-selector": [
true,
"attribute",
"app",
"camelCase"
],
"component-selector": [
true,
"element",
"app",
"kebab-case"
]
}
}

16
tsconfig.app.json Normal file
View file

@ -0,0 +1,16 @@
/* To learn more about this file see: https://angular.io/config/tsconfig. */
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "./out-tsc/app",
"types": [
"node"
]
},
"files": [
"src/main.ts"
],
"include": [
"src/**/*.d.ts"
]
}

View file

@ -1,21 +1,35 @@
/* To learn more about this file see: https://angular.io/config/tsconfig. */
{ {
"compileOnSave": false, "compileOnSave": false,
"compilerOptions": { "compilerOptions": {
"baseUrl": "./",
"outDir": "./dist/out-tsc", "outDir": "./dist/out-tsc",
"forceConsistentCasingInFileNames": true,
"strict": true,
"noImplicitOverride": true,
"noPropertyAccessFromIndexSignature": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"skipLibCheck": true,
"esModuleInterop": true,
"sourceMap": true, "sourceMap": true,
"declaration": false, "declaration": false,
"module": "es2015",
"moduleResolution": "node",
"emitDecoratorMetadata": true,
"experimentalDecorators": true, "experimentalDecorators": true,
"target": "es5", "moduleResolution": "node",
"typeRoots": [ "importHelpers": true,
"node_modules/@types" "target": "ES2022",
], "module": "ES2022",
"useDefineForClassFields": false,
"lib": [ "lib": [
"es2017", "ES2022",
"dom" "dom"
] ],
"resolveJsonModule": true,
"allowSyntheticDefaultImports": true
},
"angularCompilerOptions": {
"enableI18nLegacyMessageIdFormat": false,
"strictInjectionParameters": true,
"strictInputAccessModifiers": true,
"strictTemplates": true
} }
} }

14
tsconfig.spec.json Normal file
View file

@ -0,0 +1,14 @@
/* To learn more about this file see: https://angular.io/config/tsconfig. */
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "./out-tsc/spec",
"types": [
"jasmine"
]
},
"include": [
"src/**/*.spec.ts",
"src/**/*.d.ts"
]
}

View file

@ -1,130 +0,0 @@
{
"rulesDirectory": [
"node_modules/codelyzer"
],
"rules": {
"arrow-return-shorthand": true,
"callable-types": true,
"class-name": true,
"comment-format": [
true,
"check-space"
],
"curly": true,
"deprecation": {
"severity": "warn"
},
"eofline": true,
"forin": true,
"import-blacklist": [
true,
"rxjs/Rx"
],
"import-spacing": true,
"indent": [
true,
"spaces"
],
"interface-over-type-literal": true,
"label-position": true,
"max-line-length": [
true,
140
],
"member-access": false,
"member-ordering": [
true,
{
"order": [
"static-field",
"instance-field",
"static-method",
"instance-method"
]
}
],
"no-arg": true,
"no-bitwise": true,
"no-console": [
true,
"debug",
"info",
"time",
"timeEnd",
"trace"
],
"no-construct": true,
"no-debugger": true,
"no-duplicate-super": true,
"no-empty": false,
"no-empty-interface": true,
"no-eval": true,
"no-inferrable-types": [
true,
"ignore-params"
],
"no-misused-new": true,
"no-non-null-assertion": true,
"no-shadowed-variable": true,
"no-string-literal": false,
"no-string-throw": true,
"no-switch-case-fall-through": true,
"no-trailing-whitespace": true,
"no-unnecessary-initializer": true,
"no-unused-expression": true,
"no-use-before-declare": true,
"no-var-keyword": true,
"object-literal-sort-keys": false,
"one-line": [
true,
"check-open-brace",
"check-catch",
"check-else",
"check-whitespace"
],
"prefer-const": true,
"quotemark": [
true,
"single"
],
"radix": true,
"semicolon": [
true,
"always"
],
"triple-equals": [
true,
"allow-null-check"
],
"typedef-whitespace": [
true,
{
"call-signature": "nospace",
"index-signature": "nospace",
"parameter": "nospace",
"property-declaration": "nospace",
"variable-declaration": "nospace"
}
],
"unified-signatures": true,
"variable-name": false,
"whitespace": [
true,
"check-branch",
"check-decl",
"check-operator",
"check-separator",
"check-type"
],
"no-output-on-prefix": true,
"use-input-property-decorator": true,
"use-output-property-decorator": true,
"use-host-property-decorator": true,
"no-input-rename": true,
"no-output-rename": true,
"use-life-cycle-interface": true,
"use-pipe-transform-interface": true,
"component-class-suffix": true,
"directive-class-suffix": true
}
}