diff --git a/angular.json b/angular.json index 294d8ca4..e6d43e86 100644 --- a/angular.json +++ b/angular.json @@ -4599,6 +4599,133 @@ } } } + }, + "unit-tests-solution": { + "root": "steps/unit-tests-solution/", + "sourceRoot": "steps/unit-tests-solution/src", + "projectType": "application", + "prefix": "app", + "schematics": {}, + "architect": { + "build": { + "builder": "@angular-devkit/build-angular:browser", + "options": { + "outputPath": "dist/unit-tests-solution", + "index": "steps/unit-tests-solution/src/index.html", + "main": "steps/unit-tests-solution/src/main.ts", + "polyfills": "steps/unit-tests-solution/src/polyfills.ts", + "tsConfig": "steps/unit-tests-solution/tsconfig.app.json", + "assets": [ + "steps/unit-tests-solution/src/favicon.ico", + "steps/unit-tests-solution/src/assets" + ], + "styles": [ + "steps/unit-tests-solution/src/styles.css" + ], + "scripts": [] + }, + "configurations": { + "production": { + "fileReplacements": [ + { + "replace": "steps/unit-tests-solution/src/environments/environment.ts", + "with": "steps/unit-tests-solution/src/environments/environment.prod.ts" + } + ], + "optimization": true, + "outputHashing": "all", + "sourceMap": false, + "extractCss": true, + "namedChunks": false, + "aot": true, + "extractLicenses": true, + "vendorChunk": false, + "buildOptimizer": true, + "budgets": [ + { + "type": "initial", + "maximumWarning": "2mb", + "maximumError": "5mb" + } + ] + } + } + }, + "serve": { + "builder": "@angular-devkit/build-angular:dev-server", + "options": { + "browserTarget": "unit-tests-solution:build" + }, + "configurations": { + "production": { + "browserTarget": "unit-tests-solution:build:production" + } + } + }, + "extract-i18n": { + "builder": "@angular-devkit/build-angular:extract-i18n", + "options": { + "browserTarget": "unit-tests-solution:build" + } + }, + "test": { + "builder": "@angular-devkit/build-angular:karma", + "options": { + "main": "steps/unit-tests-solution/src/test.ts", + "polyfills": "steps/unit-tests-solution/src/polyfills.ts", + "tsConfig": "steps/unit-tests-solution/tsconfig.spec.json", + "karmaConfig": "steps/unit-tests-solution/karma.conf.js", + "styles": [ + "steps/unit-tests-solution/src/styles.css" + ], + "scripts": [], + "assets": [ + "steps/unit-tests-solution/src/favicon.ico", + "steps/unit-tests-solution/src/assets" + ] + } + }, + "lint": { + "builder": "@angular-devkit/build-angular:tslint", + "options": { + "tsConfig": [ + "steps/unit-tests-solution/tsconfig.app.json", + "steps/unit-tests-solution/tsconfig.spec.json" + ], + "exclude": [ + "**/node_modules/**" + ] + } + } + } + }, + "unit-tests-solution-e2e": { + "root": "steps/unit-tests-solution-e2e/", + "projectType": "application", + "prefix": "", + "architect": { + "e2e": { + "builder": "@angular-devkit/build-angular:protractor", + "options": { + "protractorConfig": "steps/unit-tests-solution-e2e/protractor.conf.js", + "devServerTarget": "unit-tests-solution:serve" + }, + "configurations": { + "production": { + "devServerTarget": "unit-tests-solution:serve:production" + } + } + }, + "lint": { + "builder": "@angular-devkit/build-angular:tslint", + "options": { + "tsConfig": "steps/unit-tests-solution-e2e/tsconfig.e2e.json", + "exclude": [ + "**/node_modules/**" + ] + } + } + } } }, "defaultProject": "angular-200", diff --git a/steps/unit-tests-solution/browserslist b/steps/unit-tests-solution/browserslist new file mode 100644 index 00000000..37371cb0 --- /dev/null +++ b/steps/unit-tests-solution/browserslist @@ -0,0 +1,11 @@ +# 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 \ No newline at end of file diff --git a/steps/unit-tests-solution/karma.conf.js b/steps/unit-tests-solution/karma.conf.js new file mode 100644 index 00000000..1a4dd5cf --- /dev/null +++ b/steps/unit-tests-solution/karma.conf.js @@ -0,0 +1,31 @@ +// 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 + }); +}; diff --git a/steps/unit-tests-solution/src/app/app.component.css b/steps/unit-tests-solution/src/app/app.component.css new file mode 100644 index 00000000..b06df838 --- /dev/null +++ b/steps/unit-tests-solution/src/app/app.component.css @@ -0,0 +1,46 @@ +mat-toolbar.extend-toolbar { + background-color: #0168ab; + background-image: url('/assets/images/bg_right.png'); + background-repeat: no-repeat; + background-position: right; + top: 0px; + box-shadow: 0 2px 5px 0 rgba(0, 0, 0, 0.26); + width: 100%; + z-index: 1; + color: white; + margin-bottom: 10px; +} + +mat-toolbar .flex { + flex: 1 1 auto; +} + +mat-toolbar a { + color: inherit; + text-decoration: none; + height: 100%; + margin: 0px 10px 0px 10px; + border-bottom: 2px solid transparent; + font-size: 1.1em; + font-weight: normal; + font-family: 'Open Sans', sans-serif; +} + +mat-toolbar a:not(.active):hover { + border-bottom: 2px solid white; +} + +mat-toolbar a img { + height: 100%; + margin-left: -60px; +} + +mat-toolbar-row:nth-child(1) { + margin-bottom: 66px; +} +mat-toolbar-row:nth-child(2) { + padding-left: 30px; + text-align: center; + height: 56px; + margin-top: 56px; +} diff --git a/steps/unit-tests-solution/src/app/app.component.html b/steps/unit-tests-solution/src/app/app.component.html new file mode 100644 index 00000000..ab3481ce --- /dev/null +++ b/steps/unit-tests-solution/src/app/app.component.html @@ -0,0 +1,11 @@ + + + Sfeir + + + + + Maps List + + + diff --git a/steps/unit-tests-solution/src/app/app.component.ts b/steps/unit-tests-solution/src/app/app.component.ts new file mode 100644 index 00000000..95aa5c88 --- /dev/null +++ b/steps/unit-tests-solution/src/app/app.component.ts @@ -0,0 +1,10 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'sfeir-app', + templateUrl: './app.component.html', + styleUrls: ['./app.component.css'] +}) +export class PeopleAppComponent { + constructor() {} +} diff --git a/steps/unit-tests-solution/src/app/app.module.ts b/steps/unit-tests-solution/src/app/app.module.ts new file mode 100644 index 00000000..7679cfee --- /dev/null +++ b/steps/unit-tests-solution/src/app/app.module.ts @@ -0,0 +1,67 @@ +// CORE DEPS +import { NgModule } from '@angular/core'; +import { BrowserModule } from '@angular/platform-browser'; +import { HttpClientModule } from '@angular/common/http'; +import { ReactiveFormsModule } from '@angular/forms'; +// MATERIAL DESIGN MODULES +import { + MatToolbarModule, + MatCardModule, + MatTabsModule, + MatButtonModule, + MatInputModule, + MatCheckboxModule, + MatRadioModule, + MatIconModule, + MatListModule, + MatDialogModule +} from '@angular/material'; +import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; + +import { APP_ROUTES } from './app.routes'; + +import { PeopleAppComponent } from './app.component'; +import { HomeComponent } from './home'; +import { PeopleComponent } from './people'; +import { CardComponent } from './shared/card'; +import { AddDialogComponent } from './people/add-dialog/add-dialog.component'; +import { FormComponent } from './shared/form'; +import { UpdateComponent } from './update/update.component'; +import { PeopleService } from './shared/people-service'; +import { NaPipe } from './shared/na-pipe'; +import { SfeirBadgeDirective } from './shared/badge'; + +@NgModule({ + imports: [ + BrowserModule, + BrowserAnimationsModule, + MatToolbarModule, + MatCardModule, + MatTabsModule, + MatButtonModule, + MatInputModule, + MatCheckboxModule, + MatRadioModule, + MatIconModule, + MatListModule, + MatDialogModule, + HttpClientModule, + APP_ROUTES, + ReactiveFormsModule + ], + declarations: [ + PeopleAppComponent, + HomeComponent, + PeopleComponent, + CardComponent, + AddDialogComponent, + FormComponent, + UpdateComponent, + NaPipe, + SfeirBadgeDirective + ], + entryComponents: [AddDialogComponent], + providers: [PeopleService], + bootstrap: [PeopleAppComponent] +}) +export class AppModule {} diff --git a/steps/unit-tests-solution/src/app/app.routes.ts b/steps/unit-tests-solution/src/app/app.routes.ts new file mode 100644 index 00000000..1406a666 --- /dev/null +++ b/steps/unit-tests-solution/src/app/app.routes.ts @@ -0,0 +1,15 @@ +import { RouterModule, Routes } from '@angular/router'; + +// APP COMPONENTS +import { HomeComponent } from './home/index'; +import { PeopleComponent } from './people/index'; +import { UpdateComponent } from './update/index'; + +const ROUTES: Routes = [ + { path: '', redirectTo: 'home', pathMatch: 'full' }, + { path: 'home', component: HomeComponent }, + { path: 'people', component: PeopleComponent }, + { path: 'edit/:id', component: UpdateComponent } +]; + +export const APP_ROUTES = RouterModule.forRoot(ROUTES, { useHash: true }); diff --git a/steps/unit-tests-solution/src/app/home/home.component.css b/steps/unit-tests-solution/src/app/home/home.component.css new file mode 100644 index 00000000..f9428ad8 --- /dev/null +++ b/steps/unit-tests-solution/src/app/home/home.component.css @@ -0,0 +1,10 @@ +h1 { + text-align: center; +} + +section { + display: flex; + flex-flow: row wrap; + align-items: center; + justify-content: center; +} diff --git a/steps/unit-tests-solution/src/app/home/home.component.html b/steps/unit-tests-solution/src/app/home/home.component.html new file mode 100644 index 00000000..6a1dd6c9 --- /dev/null +++ b/steps/unit-tests-solution/src/app/home/home.component.html @@ -0,0 +1,3 @@ +
+ + diff --git a/steps/unit-tests-solution/src/app/home/home.component.ts b/steps/unit-tests-solution/src/app/home/home.component.ts new file mode 100644 index 00000000..b213645e --- /dev/null +++ b/steps/unit-tests-solution/src/app/home/home.component.ts @@ -0,0 +1,34 @@ +import { Component, OnInit } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { environment } from '../../environments/environment'; + +const BASE_URL = 'http://localhost:9000'; + +@Component({ + selector: 'sfeir-home', + templateUrl: './home.component.html', + styleUrls: ['./home.component.css'] +}) +export class HomeComponent implements OnInit { + private _person: any = {}; + + constructor(private _http: HttpClient) {} + + /** + * OnInit implementation + */ + ngOnInit() { + this._http.get(`${BASE_URL}/api/peoples/`).subscribe(people => (this._person = people[0])); + } + + /** + * Returns random people + */ + random() { + this._http.get(`${BASE_URL}/api/peoples/random`).subscribe(person => (this._person = person)); + } + + get person(): any { + return this._person; + } +} diff --git a/steps/unit-tests-solution/src/app/home/index.ts b/steps/unit-tests-solution/src/app/home/index.ts new file mode 100644 index 00000000..ab5a522c --- /dev/null +++ b/steps/unit-tests-solution/src/app/home/index.ts @@ -0,0 +1 @@ +export * from './home.component'; diff --git a/steps/unit-tests-solution/src/app/index.ts b/steps/unit-tests-solution/src/app/index.ts new file mode 100644 index 00000000..875bdb2f --- /dev/null +++ b/steps/unit-tests-solution/src/app/index.ts @@ -0,0 +1,2 @@ +export * from './app.component'; +export * from './app.module'; diff --git a/steps/unit-tests-solution/src/app/people/add-dialog/add-dialog.component.css b/steps/unit-tests-solution/src/app/people/add-dialog/add-dialog.component.css new file mode 100644 index 00000000..e69de29b diff --git a/steps/unit-tests-solution/src/app/people/add-dialog/add-dialog.component.html b/steps/unit-tests-solution/src/app/people/add-dialog/add-dialog.component.html new file mode 100644 index 00000000..00b10c2c --- /dev/null +++ b/steps/unit-tests-solution/src/app/people/add-dialog/add-dialog.component.html @@ -0,0 +1 @@ + diff --git a/steps/unit-tests-solution/src/app/people/add-dialog/add-dialog.component.spec.ts b/steps/unit-tests-solution/src/app/people/add-dialog/add-dialog.component.spec.ts new file mode 100644 index 00000000..55d4e4d0 --- /dev/null +++ b/steps/unit-tests-solution/src/app/people/add-dialog/add-dialog.component.spec.ts @@ -0,0 +1,53 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { AddDialogComponent } from './add-dialog.component'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { MatDialogRef, MatDialogModule } from '@angular/material'; +import { inject } from '@angular/core/testing'; + +class MatDialogRefMock { + close() {} +} +describe('AddDialogComponent', () => { + let component: AddDialogComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [MatDialogModule], + declarations: [AddDialogComponent], + schemas: [NO_ERRORS_SCHEMA], + providers: [{ provide: MatDialogRef, useClass: MatDialogRefMock }] + }); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(AddDialogComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should close dialog', () => { + spyOn(component, 'closeDialog'); + component.onCancel(); + + expect(component.closeDialog).toHaveBeenCalled(); + }); + + it('should close dialog and pass person value', () => { + const person = { id: '1234' }; + spyOn(component, 'closeDialog'); + component.onSave(person); + expect(component.closeDialog).toHaveBeenCalledWith(person); + }); + + it('closeDialog should call closing service of dialog', inject([MatDialogRef], dialogRef => { + const result = 'ABC'; + spyOn(dialogRef, 'close'); + component.closeDialog(result); + expect(dialogRef.close).toHaveBeenCalledWith(result); + })); +}); diff --git a/steps/unit-tests-solution/src/app/people/add-dialog/add-dialog.component.ts b/steps/unit-tests-solution/src/app/people/add-dialog/add-dialog.component.ts new file mode 100644 index 00000000..2710287e --- /dev/null +++ b/steps/unit-tests-solution/src/app/people/add-dialog/add-dialog.component.ts @@ -0,0 +1,25 @@ +import { Component, OnInit } from '@angular/core'; +import { MatDialogRef } from '@angular/material'; + +@Component({ + selector: 'sfeir-add-dialog', + templateUrl: './add-dialog.component.html', + styleUrls: ['./add-dialog.component.css'] +}) +export class AddDialogComponent implements OnInit { + constructor(public dialogRef: MatDialogRef) {} + + closeDialog(result = null) { + this.dialogRef.close(result); + } + + ngOnInit() {} + + onCancel() { + this.closeDialog(); + } + + onSave(person) { + this.closeDialog(person); + } +} diff --git a/steps/unit-tests-solution/src/app/people/index.ts b/steps/unit-tests-solution/src/app/people/index.ts new file mode 100644 index 00000000..469a1b27 --- /dev/null +++ b/steps/unit-tests-solution/src/app/people/index.ts @@ -0,0 +1 @@ +export * from './people.component'; diff --git a/steps/unit-tests-solution/src/app/people/people.component.css b/steps/unit-tests-solution/src/app/people/people.component.css new file mode 100644 index 00000000..e896bb5d --- /dev/null +++ b/steps/unit-tests-solution/src/app/people/people.component.css @@ -0,0 +1,65 @@ +h1 { + text-align: center; +} + +section { + display: flex; + flex-flow: row wrap; + align-items: center; + justify-content: center; +} + +.list { + padding-bottom: 100px; +} + +.dialog-modal { + top: 0; + position: fixed; + background: rgba(0, 0, 0, 0.5); + height: 100%; + width: 100%; + display: flex; + flex-basis: auto; + visibility: hidden; + color: white; +} +.dialog-modal.active { + visibility: visible; +} + +mat-list-item { + border: 1px solid rgba(0, 0, 0, 0.1); + color: rgba(0, 0, 0, 0.87); + background-color: white; + margin-left: -12px; + font-size: 14px; + display: block; + margin: 3px 0; + padding: 0; + height: 70px; + width: 400px; + transition: box-shadow 0.2s; + cursor: pointer; +} + +mat-list-item:hover { + box-shadow: 0 24px 38px 3px rgba(0, 0, 0, 0.14), 0 9px 46px 8px rgba(0, 0, 0, 0.12); +} + +.buttons-fab { + display: block; + height: 170px; + width: 90px; + position: fixed; + bottom: 0; + right: 0; +} + +.buttons-fab button { + transition: bottom 0.2s; +} + +.buttons-fab:hover .button-add { + bottom: 90px; +} diff --git a/steps/unit-tests-solution/src/app/people/people.component.html b/steps/unit-tests-solution/src/app/people/people.component.html new file mode 100644 index 00000000..a4a25fd9 --- /dev/null +++ b/steps/unit-tests-solution/src/app/people/people.component.html @@ -0,0 +1,26 @@ +
+
+ +
+ +
+ + + +

{{ person.firstname }} {{ person.lastname }}

+

+ {{ person.entity }} {{ person.email }} +

+
+
+
+
+ +
+ + + +
diff --git a/steps/unit-tests-solution/src/app/people/people.component.ts b/steps/unit-tests-solution/src/app/people/people.component.ts new file mode 100644 index 00000000..17f3cfb2 --- /dev/null +++ b/steps/unit-tests-solution/src/app/people/people.component.ts @@ -0,0 +1,65 @@ +import { Component, OnInit } from '@angular/core'; +import { MatDialog, MatDialogRef, MAT_DIALOG_DATA } from '@angular/material'; +import { AddDialogComponent } from './add-dialog/add-dialog.component'; +import { PeopleService } from '../shared/people-service'; +import { HttpClient } from '@angular/common/http'; +import { mergeMap } from 'rxjs/operators'; + +@Component({ + selector: 'sfeir-people', + templateUrl: './people.component.html', + styleUrls: ['./people.component.css'] +}) +export class PeopleComponent implements OnInit { + private addDialog: MatDialogRef; + people; + dialogStatus = 'inactive'; + view = 'card'; + + constructor(private _http: HttpClient, public dialog: MatDialog, private _peopleService: PeopleService) {} + + /** + * OnInit implementation + */ + ngOnInit() { + this._peopleService.fetch().subscribe(people => (this.people = people)); + } + + delete(person: any) { + this._peopleService.delete(person.id).subscribe(people => (this.people = people)); + } + + add(person: any) { + this._peopleService + .update(person) + .pipe(mergeMap((updatedPerson: any) => this._peopleService.fetch())) + .subscribe((peopleList: Array) => { + this.people = peopleList; + this.hideDialog(); + }); + } + + showDialog() { + this.dialogStatus = 'active'; + this.addDialog = this.dialog.open(AddDialogComponent, { + width: '450px', + data: {} + }); + + this.addDialog.afterClosed().subscribe(person => { + this.dialogStatus = 'inactive'; + if (person) { + this.add(person); + } + }); + } + + hideDialog() { + this.dialogStatus = 'inactive'; + this.addDialog.close(); + } + + switchView() { + this.view = this.view === 'card' ? 'list' : 'card'; + } +} diff --git a/steps/unit-tests-solution/src/app/shared/badge/index.ts b/steps/unit-tests-solution/src/app/shared/badge/index.ts new file mode 100644 index 00000000..33171a44 --- /dev/null +++ b/steps/unit-tests-solution/src/app/shared/badge/index.ts @@ -0,0 +1 @@ +export * from './sfeir-badge.directive'; diff --git a/steps/unit-tests-solution/src/app/shared/badge/sfeir-badge.directive.spec.ts b/steps/unit-tests-solution/src/app/shared/badge/sfeir-badge.directive.spec.ts new file mode 100644 index 00000000..2894f4e6 --- /dev/null +++ b/steps/unit-tests-solution/src/app/shared/badge/sfeir-badge.directive.spec.ts @@ -0,0 +1,62 @@ +import { BrowserModule, By } from '@angular/platform-browser'; +import { inject } from '@angular/core/testing'; +/* tslint:disable:no-unused-variable */ + +import { TestBed, async, ComponentFixture } from '@angular/core/testing'; +import { SfeirBadgeDirective } from './sfeir-badge.directive'; +import { ElementRef, Renderer, Component } from '@angular/core'; + +export class MockElementRef extends ElementRef { + constructor() { + super(null); + } +} + +export class MockRenderer { + setElementProperty(...args) {} +} + +@Component({ + selector: 'test-badge-directive', + template: `` +}) +export class HostComponentForBadgeDirective { + person = {}; +} + +const MANAGER_BADGE_HTML = 'supervisor_account'; + +describe('SfeirBadgeDirective', () => { + beforeEach(() => { + TestBed.configureTestingModule({ + declarations: [HostComponentForBadgeDirective, SfeirBadgeDirective] + }); + }); + + it('should create an instance of sfeir-badge', () => { + const fixture = createTestComponent('
'); + expect(fixture).toBeDefined(); + }); + + it('should not add badge icon when isManager === false', () => { + const fixture = createTestComponent('
'); + fixture.componentInstance.person = { isManager: false }; + fixture.detectChanges(); + const divElement = fixture.nativeElement.querySelector('div'); + expect(divElement.innerHTML).toBe(''); + }); + + it('should add badge icon when isManager === true', () => { + const fixture = createTestComponent('
'); + fixture.componentInstance.person = { isManager: true }; + fixture.detectChanges(); + const divElement = fixture.nativeElement.querySelector('div'); + expect(divElement.innerHTML).toBe(MANAGER_BADGE_HTML); + }); +}); + +function createTestComponent(template: string): ComponentFixture { + return TestBed.overrideComponent(HostComponentForBadgeDirective, { set: { template } }).createComponent( + HostComponentForBadgeDirective + ); +} diff --git a/steps/unit-tests-solution/src/app/shared/badge/sfeir-badge.directive.ts b/steps/unit-tests-solution/src/app/shared/badge/sfeir-badge.directive.ts new file mode 100644 index 00000000..10a221c5 --- /dev/null +++ b/steps/unit-tests-solution/src/app/shared/badge/sfeir-badge.directive.ts @@ -0,0 +1,25 @@ +import { Directive, ElementRef, Renderer, Input, OnInit } from '@angular/core'; + +@Directive({ + selector: '[sfeir-badge]' +}) +export class SfeirBadgeDirective implements OnInit { + @Input() person: any; + + /** + * Component constructor + */ + constructor(private _el: ElementRef, private _rd: Renderer) {} + /** + * OnInit implementation + */ + ngOnInit() { + if (this.person && this.person.isManager) { + this._rd.setElementProperty( + this._el.nativeElement, + 'innerHTML', + 'supervisor_account' + ); + } + } +} diff --git a/steps/unit-tests-solution/src/app/shared/card/card.component.css b/steps/unit-tests-solution/src/app/shared/card/card.component.css new file mode 100644 index 00000000..8febb8ac --- /dev/null +++ b/steps/unit-tests-solution/src/app/shared/card/card.component.css @@ -0,0 +1,137 @@ +@media (min-width: 768px) { + :host(.wide) mat-card, + :host(.wide) mat-list-item { + width: 600px; + } +} + +mat-card, +mat-list-item { + color: rgba(0, 0, 0, 0.87); + background-color: white; + margin: 10px; + width: 360px; +} + +mat-card:hover { + box-shadow: 0 11px 15px -7px rgba(0, 0, 0, 0.2), 0 24px 38px 3px rgba(0, 0, 0, 0.14), + 0 9px 46px 8px rgba(0, 0, 0, 0.12); +} + +mat-list-item { + height: 22px; + padding: 0; + margin-left: -12px; + font-size: 14px; +} + +mat-card-title { + margin-bottom: 0; +} + +mat-card-title span { + font-size: 24px; + font-weight: 400; + line-height: 32px; +} + +mat-card-subtitle.contact-info { + margin-top: -7px; + height: 20px; +} + +mat-card-title-group { + margin-bottom: 20px; +} + +.contact-info mat-icon + a { + top: -8px; + position: relative; + height: 18px; +} + +.contact-info a:hover { + text-decoration: underline; +} + +.buttons-info { + float: right; + margin-top: -50px; +} + +@media (max-width: 412px) { + .buttons-info { + margin-top: -34px; + } +} + +.buttons-info [mat-button] { + min-width: 0px; + padding: 2px; + height: 40px; +} + +mat-icon { + color: rgba(0, 0, 0, 0.54); + margin: 4px; +} + +a { + color: #337ab7; + text-decoration: none; +} + +mat-card-actions button[mat-raised-button] { + margin: 5px; +} + +.hl { + background-color: orange; + color: white; +} + +.skills { + padding: 10px; + background-color: #fafafa; +} + +a.truncate { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + display: inline-block; + width: 188px; +} + +@media (max-width: 600px) { + a.truncate { + width: 170px; + } +} + +@media (max-width: 768px) { + a.truncate { + width: 190px; + } +} + +.mat-whiteframe-2dp { + box-shadow: 0 1px 5px 0 rgba(0, 0, 0, 0.2), 0 2px 2px 0 rgba(0, 0, 0, 0.14), 0 3px 1px -2px rgba(0, 0, 0, 0.12); +} + +img { + border-radius: 50%; + margin-right: 18px; +} + +@media (max-width: 412px) { + img { + margin-right: 3px; + } +} + +@media (max-width: 600px) { + img { + margin-right: 5px; + } +} diff --git a/steps/unit-tests-solution/src/app/shared/card/card.component.html b/steps/unit-tests-solution/src/app/shared/card/card.component.html new file mode 100644 index 00000000..515c77c4 --- /dev/null +++ b/steps/unit-tests-solution/src/app/shared/card/card.component.html @@ -0,0 +1,35 @@ + + + + + + {{ person.firstname }} {{ person.lastname }} + + + {{ person.entity | na }} + + email + {{ person.email }} + + + phone + {{ person.phone }} + + + +
+ Manager :  {{ person.manager }} +
+
Location : SFEIR
+ +
+
diff --git a/steps/unit-tests-solution/src/app/shared/card/card.component.ts b/steps/unit-tests-solution/src/app/shared/card/card.component.ts new file mode 100644 index 00000000..7bb6744a --- /dev/null +++ b/steps/unit-tests-solution/src/app/shared/card/card.component.ts @@ -0,0 +1,26 @@ +import { Component, OnInit, Input, EventEmitter, Output } from '@angular/core'; + +@Component({ + selector: 'sfeir-card', + templateUrl: './card.component.html', + styleUrls: ['./card.component.css'] +}) +export class CardComponent implements OnInit { + @Input() person: any; + @Output('personDelete') delete$: EventEmitter = new EventEmitter(); + + constructor() {} + + /** + * OnInit implementation + */ + ngOnInit() {} + + /** + * Function to emit event to delete current person + * + */ + delete(person: any) { + this.delete$.emit(person); + } +} diff --git a/steps/unit-tests-solution/src/app/shared/card/index.ts b/steps/unit-tests-solution/src/app/shared/card/index.ts new file mode 100644 index 00000000..54bbbca7 --- /dev/null +++ b/steps/unit-tests-solution/src/app/shared/card/index.ts @@ -0,0 +1 @@ +export * from './card.component'; diff --git a/steps/unit-tests-solution/src/app/shared/form/custom-validators.ts b/steps/unit-tests-solution/src/app/shared/form/custom-validators.ts new file mode 100644 index 00000000..e5142a3b --- /dev/null +++ b/steps/unit-tests-solution/src/app/shared/form/custom-validators.ts @@ -0,0 +1,19 @@ +import { FormControl } from '@angular/forms'; + +export class CustomValidators { + /** + * Function to control email with custom validator + * + */ + static sfeirEmail(control: FormControl) { + // email regex + const regex = /^\w+\.\w@sfeir\.com$/; + + // returns control + return regex.test(control.value) + ? null + : { + sfeirEmail: true + }; + } +} diff --git a/steps/unit-tests-solution/src/app/shared/form/form.component.css b/steps/unit-tests-solution/src/app/shared/form/form.component.css new file mode 100644 index 00000000..7ddb2dc6 --- /dev/null +++ b/steps/unit-tests-solution/src/app/shared/form/form.component.css @@ -0,0 +1,51 @@ +mat-card, +mat-list-item { + color: rgba(0, 0, 0, 0.87); + background-color: white; + width: 360px; +} + +mat-card:hover { + box-shadow: 0 11px 15px -7px rgba(0, 0, 0, 0.2), 0 24px 38px 3px rgba(0, 0, 0, 0.14), + 0 9px 46px 8px rgba(0, 0, 0, 0.12); +} + +mat-list-item { + height: 22px; + padding: 0; + margin-left: -12px; + font-size: 14px; +} + +mat-card-title { + margin-bottom: 0; +} + +mat-card-title span { + font-size: 24px; + font-weight: 400; + line-height: 32px; +} + +mat-card-subtitle { + margin-top: -18px; + height: 17px; +} + +mat-card-title-group { + margin-bottom: 20px; +} + +img { + border-radius: 50%; + margin-right: 18px; + -webkit-filter: grayscale(100%); +} + +.align-right { + text-align: right; +} + +.errors { + color: red; +} diff --git a/steps/unit-tests-solution/src/app/shared/form/form.component.html b/steps/unit-tests-solution/src/app/shared/form/form.component.html new file mode 100644 index 00000000..58436f98 --- /dev/null +++ b/steps/unit-tests-solution/src/app/shared/form/form.component.html @@ -0,0 +1,108 @@ + + + Update {{ form.controls['firstname'].value }} {{ form.controls['firstname'].value }} + Create new person + + + +
+

+ ID (disabled)
+ {{ model.id }} +

+ + + + + + +
+

+ + + + +
First name is required
+
First name is 2 chars min
+

+ +

+ + + + +
Last name is required
+
Last name is 2 chars min
+

+
+ + +
+ +

+ + + + + Email is mandatory +
Invalid email (ex: chegham.w@sfeir.com)
+

+ +

+ + + +

+ +

+ + + + +

+ +

+ + + + + Phone number is mandatory + Phone number must be 10 digits +

+ +

Manager

+
+
+ + + + + +
diff --git a/steps/unit-tests-solution/src/app/shared/form/form.component.ts b/steps/unit-tests-solution/src/app/shared/form/form.component.ts new file mode 100644 index 00000000..df876fbb --- /dev/null +++ b/steps/unit-tests-solution/src/app/shared/form/form.component.ts @@ -0,0 +1,79 @@ +import { Component, OnInit, Output, Input, EventEmitter, OnChanges } from '@angular/core'; +import { FormControl, Validators, FormGroup } from '@angular/forms'; +import { CustomValidators } from './custom-validators'; + +@Component({ + selector: 'sfeir-form', + templateUrl: './form.component.html', + styleUrls: ['./form.component.css'] +}) +export class FormComponent implements OnInit, OnChanges { + private _form: FormGroup; + @Input() model: any; + isUpdateMode: boolean; + @Output('cancel') cancel$: EventEmitter; + @Output('submit') submit$: EventEmitter; + + constructor() { + this.submit$ = new EventEmitter(); + this.cancel$ = new EventEmitter(); + this.model = { address: {} }; + this._form = this._buildForm(); + } + /** + * OnInit implementation + */ + ngOnInit() {} + + /** + * Function to handle component update + * + */ + ngOnChanges(record) { + if (record.model && record.model.currentValue) { + this.model = record.model.currentValue; + this.isUpdateMode = !!this.model; + this._form.patchValue(this.model); + } + } + + /** + * Function to emit event to cancel process + */ + cancel() { + this.cancel$.emit(); + } + + /** + * Function to emit event to submit form and person + */ + submit(person: any) { + this.submit$.emit(person); + } + + /** + * Function to build our form + * + * + */ + private _buildForm(): FormGroup { + return new FormGroup({ + id: new FormControl(''), + firstname: new FormControl('', Validators.compose([Validators.required, Validators.minLength(2)])), + lastname: new FormControl('', Validators.compose([Validators.required, Validators.minLength(2)])), + email: new FormControl('', Validators.compose([Validators.required, CustomValidators.sfeirEmail])), + photo: new FormControl('https://randomuser.me/api/portraits/lego/6.jpg'), + address: new FormGroup({ + street: new FormControl(''), + city: new FormControl(''), + postalCode: new FormControl('') + }), + phone: new FormControl('', Validators.compose([Validators.required, Validators.pattern('\\d{10}')])), + isManager: new FormControl('') + }); + } + + get form(): FormGroup { + return this._form; + } +} diff --git a/steps/unit-tests-solution/src/app/shared/form/index.ts b/steps/unit-tests-solution/src/app/shared/form/index.ts new file mode 100644 index 00000000..780c593b --- /dev/null +++ b/steps/unit-tests-solution/src/app/shared/form/index.ts @@ -0,0 +1 @@ +export * from './form.component'; diff --git a/steps/unit-tests-solution/src/app/shared/index.ts b/steps/unit-tests-solution/src/app/shared/index.ts new file mode 100644 index 00000000..a1f450c3 --- /dev/null +++ b/steps/unit-tests-solution/src/app/shared/index.ts @@ -0,0 +1,5 @@ +export * from './badge/index'; +export * from './card/index'; +export * from './form/index'; +export * from './na-pipe/index'; +export * from './people-service/index'; diff --git a/steps/unit-tests-solution/src/app/shared/na-pipe/index.ts b/steps/unit-tests-solution/src/app/shared/na-pipe/index.ts new file mode 100644 index 00000000..029c3506 --- /dev/null +++ b/steps/unit-tests-solution/src/app/shared/na-pipe/index.ts @@ -0,0 +1 @@ +export * from './na.pipe'; diff --git a/steps/unit-tests-solution/src/app/shared/na-pipe/na.pipe.spec.ts b/steps/unit-tests-solution/src/app/shared/na-pipe/na.pipe.spec.ts new file mode 100644 index 00000000..66a69570 --- /dev/null +++ b/steps/unit-tests-solution/src/app/shared/na-pipe/na.pipe.spec.ts @@ -0,0 +1,38 @@ +/* tslint:disable:no-unused-variable */ + +import { TestBed, async } from '@angular/core/testing'; +import { NaPipe } from './na.pipe'; + +describe('NaPipe', () => { + it('create an instance', () => { + const pipe = new NaPipe(); + expect(pipe).toBeTruthy(); + }); + + describe('should return N/A...', () => { + it('for NULL', () => { + const pipe = new NaPipe(); + const result = pipe.transform(null); + expect(result).toContain('N/A'); + }); + + it('for UNDEFINED', () => { + const pipe = new NaPipe(); + const result = pipe.transform(undefined); + expect(result).toContain('N/A'); + }); + + it('for "" (empty string)', () => { + const pipe = new NaPipe(); + const result = pipe.transform(''); + expect(result).toContain('N/A'); + }); + }); + describe('Should not return N/A', () => { + it('for a given manager name: Foo Bar', () => { + const pipe = new NaPipe(); + const result = pipe.transform('Foo Bar'); + expect(result).toContain('Foo Bar'); + }); + }); +}); diff --git a/steps/unit-tests-solution/src/app/shared/na-pipe/na.pipe.ts b/steps/unit-tests-solution/src/app/shared/na-pipe/na.pipe.ts new file mode 100644 index 00000000..ae8a5198 --- /dev/null +++ b/steps/unit-tests-solution/src/app/shared/na-pipe/na.pipe.ts @@ -0,0 +1,14 @@ +import { Pipe, PipeTransform } from '@angular/core'; + +@Pipe({ + name: 'na' +}) +export class NaPipe implements PipeTransform { + /** + * Function to transform input value + * + */ + transform(value: any, args?: any): any { + return value || 'N/A'; + } +} diff --git a/steps/unit-tests-solution/src/app/shared/people-service/index.ts b/steps/unit-tests-solution/src/app/shared/people-service/index.ts new file mode 100644 index 00000000..1f73446f --- /dev/null +++ b/steps/unit-tests-solution/src/app/shared/people-service/index.ts @@ -0,0 +1 @@ +export * from './people.service'; diff --git a/steps/unit-tests-solution/src/app/shared/people-service/people.service.spec.ts b/steps/unit-tests-solution/src/app/shared/people-service/people.service.spec.ts new file mode 100644 index 00000000..69a4043f --- /dev/null +++ b/steps/unit-tests-solution/src/app/shared/people-service/people.service.spec.ts @@ -0,0 +1,167 @@ +import { TestBed } from '@angular/core/testing'; +import { PeopleService } from './people.service'; +import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; +import { HttpClient } from '@angular/common/http'; + +describe('PeopleService', () => { + const expectedResponse = [ + { + id: '123', + lastname: 'Powers', + firstname: 'Black', + twitter: 'labore' + }, + { + id: '456', + lastname: 'Shaffer', + firstname: 'Vargas', + twitter: 'irure' + }, + { + id: '789', + lastname: 'Yang', + firstname: 'Mendez', + twitter: 'excepteur' + } + ]; + + let httpTestingController: HttpTestingController; + let service: PeopleService; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule], + providers: [PeopleService] + }); + + // Inject the http service and test controller for each test + service = TestBed.get(PeopleService); + httpTestingController = TestBed.get(HttpTestingController); + }); + + afterEach(() => { + // After every test, assert that there are no more pending requests. + httpTestingController.verify(); + }); + + describe('fetch()', () => { + it('should fetch all people when status === 200', () => { + service.fetch().subscribe(response => { + expect(response.length).toEqual(3); + expect(response).toEqual(expectedResponse); + }); + + // The following `expectOne()` will match the request's URL. + // If no requests or multiple requests matched that URL + // `expectOne()` would throw. + const req = httpTestingController.expectOne(service.backendURL.allPeople); + + // Assert that the request is a GET. + expect(req.request.method).toEqual('GET'); + + // Respond with mock data, causing Observable to resolve. + // Subscribe callback asserts that correct data was returned. + req.flush(expectedResponse); + }); + + it('should return an empty array when status !== 200', () => { + const emsg = 'deliberate 404 error'; + + service.fetch().subscribe(response => { + expect(response).toEqual([]); + }, fail); + + const req = httpTestingController.expectOne(service.backendURL.allPeople); + + // Respond with mock error + req.flush(emsg, { status: 404, statusText: 'Not Found' }); + }); + }); + + describe('fetchRandom()', () => { + it('should fetch random person when status === 200', () => { + service.fetchRandom().subscribe(person => { + expect(person.id).toBe('456'); + }); + + const req = httpTestingController.expectOne(service.backendURL.randomPeople); + + req.flush(expectedResponse[1]); + }); + }); + + describe('fetchOne()', () => { + it('should fetch person with id=456 when status === 200', () => { + const id = '456'; + + service.fetchOne(id).subscribe(person => { + expect(person.id).toBe(id); + }); + + const req = httpTestingController.expectOne(service.backendURL.onePeople.replace(':id', id)); + + req.flush(expectedResponse[1]); + }); + }); + + describe('delete()', () => { + it('should delete person with id=456 when status === 200', () => { + const id = '456'; + + service.delete(id).subscribe(response => { + expect(response.length).toBe(2); + expect(response[0].id).toBe('123'); + expect(response[1].id).toBe('789'); + }); + + const req = httpTestingController.expectOne(service.backendURL.onePeople.replace(':id', id)); + + req.flush([expectedResponse[0], ...expectedResponse.slice(2)]); // remove entry=1 + }); + }); + + describe('update()', () => { + it('should update person with id=456 when status === 200', () => { + const newData = { + firstname: 'Noël', + lastname: 'Macé', + twitter: '@noel_mace' + }; + + const body = { ...expectedResponse[0], ...newData }; + + service.update(body).subscribe(person => { + expect(person.id).toBe(body.id); + expect(person.firstname).toBe(newData.firstname); + expect(person.lastname).toBe(newData.lastname); + expect(person.twitter).toBe(newData.twitter); + }); + + const req = httpTestingController.expectOne(service.backendURL.onePeople.replace(':id', body.id)); + + req.flush(body); + }); + }); + + describe('create()', () => { + it('should create person when status === 200', () => { + const body = { + id: '900', + firstname: 'Wassim', + lastname: 'Chegham', + twitter: '@manekinekko' + }; + + service.create(body).subscribe(person => { + expect(person.id).toBe('900'); + expect(person.firstname).toBe('Wassim'); + expect(person.lastname).toBe('Chegham'); + expect(person.twitter).toBe('@manekinekko'); + }); + + const req = httpTestingController.expectOne(service.backendURL.allPeople); + + req.flush(body); + }); + }); +}); diff --git a/steps/unit-tests-solution/src/app/shared/people-service/people.service.ts b/steps/unit-tests-solution/src/app/shared/people-service/people.service.ts new file mode 100644 index 00000000..3edd12b4 --- /dev/null +++ b/steps/unit-tests-solution/src/app/shared/people-service/people.service.ts @@ -0,0 +1,56 @@ +import { Injectable } from '@angular/core'; +import { HttpClient, HttpErrorResponse } from '@angular/common/http'; +import { environment } from '../../../environments/environment'; +import { catchError } from 'rxjs/operators'; +import { Observable, of } from 'rxjs'; +@Injectable() +export class PeopleService { + backendURL: any; + + constructor(private _http: HttpClient) { + this.backendURL = {}; + + // build backend base url + let baseUrl = `${environment.backend.protocol}://${environment.backend.host}`; + if (environment.backend.port) { + baseUrl += `:${environment.backend.port}`; + } + + // build all backend urls + Object.keys(environment.backend.endpoints).forEach( + k => (this.backendURL[k] = `${baseUrl}${environment.backend.endpoints[k]}`) + ); + } + + private handleError(result = {}) { + return (error: HttpErrorResponse): Observable => { + console.error(error); + // Let the app keep running by returning a safe result. + return of(result); + }; + } + + fetch(): Observable { + return this._http.get(this.backendURL.allPeople).pipe(catchError(this.handleError([]))); + } + + fetchRandom(): Observable { + return this._http.get(this.backendURL.randomPeople); + } + + fetchOne(id: string): Observable { + return this._http.get(this.backendURL.onePeople.replace(':id', id)); + } + + delete(id: string): Observable { + return this._http.delete(this.backendURL.onePeople.replace(':id', id)); + } + + update(person: any): Observable { + return this._http.put(this.backendURL.onePeople.replace(':id', person.id), person); + } + + create(person): Observable { + return this._http.post(this.backendURL.allPeople, person); + } +} diff --git a/steps/unit-tests-solution/src/app/update/index.ts b/steps/unit-tests-solution/src/app/update/index.ts new file mode 100644 index 00000000..727cfbfa --- /dev/null +++ b/steps/unit-tests-solution/src/app/update/index.ts @@ -0,0 +1,2 @@ +export * from './update.component'; +export * from './update.routes'; diff --git a/steps/unit-tests-solution/src/app/update/update.component.css b/steps/unit-tests-solution/src/app/update/update.component.css new file mode 100644 index 00000000..f9428ad8 --- /dev/null +++ b/steps/unit-tests-solution/src/app/update/update.component.css @@ -0,0 +1,10 @@ +h1 { + text-align: center; +} + +section { + display: flex; + flex-flow: row wrap; + align-items: center; + justify-content: center; +} diff --git a/steps/unit-tests-solution/src/app/update/update.component.html b/steps/unit-tests-solution/src/app/update/update.component.html new file mode 100644 index 00000000..872c9372 --- /dev/null +++ b/steps/unit-tests-solution/src/app/update/update.component.html @@ -0,0 +1 @@ +
diff --git a/steps/unit-tests-solution/src/app/update/update.component.spec.ts b/steps/unit-tests-solution/src/app/update/update.component.spec.ts new file mode 100644 index 00000000..9deecab0 --- /dev/null +++ b/steps/unit-tests-solution/src/app/update/update.component.spec.ts @@ -0,0 +1,121 @@ +/* tslint:disable:no-unused-variable */ + +import { PeopleService } from './../shared/people-service/people.service'; +import { ActivatedRoute, Router } from '@angular/router'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { By } from '@angular/platform-browser'; +import { DebugElement } from '@angular/core'; + +import { UpdateComponent } from './update.component'; +import { BehaviorSubject, Observable } from 'rxjs'; + +const expectedResponse = { + id: '123', + firstname: 'Wassim', + lastname: 'Chegham', + email: 'chegham.w@sfeir.com', + address: { + street: '48 Rue Jacques Dulud', + city: 'Neuilly-sur-Seine', + postalCode: '92200' + }, + phone: '0141385200', + isManager: false +}; + +export const mockRouter = { + navigate: jasmine.createSpy('navigate') +}; + +export class MockActivatedRoute { + private _params: {}; + private paramsSubject: BehaviorSubject; + constructor() { + this.paramsSubject = new BehaviorSubject(this._params); + } + get params() { + return this.paramsSubject.asObservable(); + } + set params(params: any) { + this._params = params; + this.paramsSubject.next(params); + } +} + +export class MockPeopleService { + fetchOne(id) { + return Observable.create(o => o.next(expectedResponse)); + } + update(person: any) { + return Observable.create(o => o.next(Object.assign(expectedResponse, person))); + } +} + +describe('UpdateComponent', () => { + let component: UpdateComponent; + let fixture: ComponentFixture; + let mockActivatedRoute: MockActivatedRoute; + let mockPeopleService: MockPeopleService; + + beforeEach(() => { + mockActivatedRoute = new MockActivatedRoute(); + mockPeopleService = new MockPeopleService(); + + TestBed.configureTestingModule({ + declarations: [UpdateComponent], + providers: [ + { provide: ActivatedRoute, useValue: mockActivatedRoute }, + { provide: Router, useValue: mockRouter }, + { provide: PeopleService, useValue: mockPeopleService } + ], + // Tells the compiler not to error on unknown elements and attributes + schemas: [NO_ERRORS_SCHEMA] + }); + }); + + it('should create an instance of UpdateComponent', () => { + fixture = TestBed.createComponent(UpdateComponent); + component = fixture.componentInstance; + mockActivatedRoute.params = { id: 123 }; + + fixture.detectChanges(); + + expect(component).toBeTruthy('The instance of UpdateComponent was not created!'); + }); + + it('should fetch person with id=123', () => { + fixture = TestBed.createComponent(UpdateComponent); + component = fixture.componentInstance; + mockActivatedRoute.params = { id: 123 }; + + fixture.detectChanges(); + + expect(component.person).toBeTruthy(); + expect(component.person.id).toBe('123'); + expect(component.person.firstname).toBe('Wassim'); + expect(component.person.lastname).toBe('Chegham'); + }); + + it('should navigate to /people when updating info', () => { + fixture = TestBed.createComponent(UpdateComponent); + component = fixture.componentInstance; + mockActivatedRoute.params = { id: 123 }; + component.submit({ isManager: true }); + + fixture.detectChanges(); + + expect(mockRouter.navigate).toHaveBeenCalledWith(['/people']); + }); + + it('should navigate to /people when cancelling', () => { + fixture = TestBed.createComponent(UpdateComponent); + component = fixture.componentInstance; + mockActivatedRoute.params = { id: 123 }; + component.cancel(); + + fixture.detectChanges(); + + expect(mockRouter.navigate).toHaveBeenCalledWith(['/people']); + }); +}); diff --git a/steps/unit-tests-solution/src/app/update/update.component.ts b/steps/unit-tests-solution/src/app/update/update.component.ts new file mode 100644 index 00000000..772d2431 --- /dev/null +++ b/steps/unit-tests-solution/src/app/update/update.component.ts @@ -0,0 +1,42 @@ +import { Component, OnInit } from '@angular/core'; +import { ActivatedRoute, Router } from '@angular/router'; +import { PeopleService } from '../shared/people-service'; +import { map, mergeMap } from 'rxjs/operators'; + +@Component({ + selector: 'sfeir-update', + templateUrl: './update.component.html', + styleUrls: ['./update.component.css'] +}) +export class UpdateComponent implements OnInit { + person: any; + + /** + * Component constructor + */ + constructor(private _route: ActivatedRoute, private _router: Router, private _peopleService: PeopleService) { + this.person = { + address: {} + }; + } + + /** + * OnInit implementation + */ + ngOnInit() { + this._route.params + .pipe( + map((params: any) => params.id), + mergeMap((id: string) => this._peopleService.fetchOne(id)) + ) + .subscribe((person: any) => (this.person = person)); + } + + submit(person: any) { + this._peopleService.update(person).subscribe(() => this._router.navigate(['/people'])); + } + + cancel() { + this._router.navigate(['/people']); + } +} diff --git a/steps/unit-tests-solution/src/app/update/update.routes.ts b/steps/unit-tests-solution/src/app/update/update.routes.ts new file mode 100644 index 00000000..686fdfd6 --- /dev/null +++ b/steps/unit-tests-solution/src/app/update/update.routes.ts @@ -0,0 +1,3 @@ +import { UpdateComponent } from './update.component'; + +export const UPDATE_ROUTES = [{ path: 'edit/:id', component: UpdateComponent }]; diff --git a/steps/unit-tests-solution/src/assets/.gitkeep b/steps/unit-tests-solution/src/assets/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/steps/unit-tests-solution/src/assets/.npmignore b/steps/unit-tests-solution/src/assets/.npmignore new file mode 100644 index 00000000..e69de29b diff --git a/steps/unit-tests-solution/src/assets/images/bg_right.png b/steps/unit-tests-solution/src/assets/images/bg_right.png new file mode 100644 index 00000000..449e4bc8 Binary files /dev/null and b/steps/unit-tests-solution/src/assets/images/bg_right.png differ diff --git a/steps/unit-tests-solution/src/assets/images/icon-delete.svg b/steps/unit-tests-solution/src/assets/images/icon-delete.svg new file mode 100644 index 00000000..c665bde5 --- /dev/null +++ b/steps/unit-tests-solution/src/assets/images/icon-delete.svg @@ -0,0 +1 @@ + diff --git a/steps/unit-tests-solution/src/assets/images/icon-edit.svg b/steps/unit-tests-solution/src/assets/images/icon-edit.svg new file mode 100644 index 00000000..bb07333e --- /dev/null +++ b/steps/unit-tests-solution/src/assets/images/icon-edit.svg @@ -0,0 +1 @@ + diff --git a/steps/unit-tests-solution/src/assets/images/icon-mail.svg b/steps/unit-tests-solution/src/assets/images/icon-mail.svg new file mode 100644 index 00000000..040a7e7f --- /dev/null +++ b/steps/unit-tests-solution/src/assets/images/icon-mail.svg @@ -0,0 +1 @@ + diff --git a/steps/unit-tests-solution/src/assets/images/icon-maps.svg b/steps/unit-tests-solution/src/assets/images/icon-maps.svg new file mode 100644 index 00000000..989697e2 --- /dev/null +++ b/steps/unit-tests-solution/src/assets/images/icon-maps.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + diff --git a/steps/unit-tests-solution/src/assets/images/icon-phone.svg b/steps/unit-tests-solution/src/assets/images/icon-phone.svg new file mode 100644 index 00000000..15ac4a3e --- /dev/null +++ b/steps/unit-tests-solution/src/assets/images/icon-phone.svg @@ -0,0 +1 @@ + diff --git a/steps/unit-tests-solution/src/assets/images/logo-sfeir.svg b/steps/unit-tests-solution/src/assets/images/logo-sfeir.svg new file mode 100644 index 00000000..9d6e4c3e --- /dev/null +++ b/steps/unit-tests-solution/src/assets/images/logo-sfeir.svg @@ -0,0 +1,18 @@ + + + + + + + + + diff --git a/steps/unit-tests-solution/src/assets/images/search-icon.svg b/steps/unit-tests-solution/src/assets/images/search-icon.svg new file mode 100644 index 00000000..92e95a18 --- /dev/null +++ b/steps/unit-tests-solution/src/assets/images/search-icon.svg @@ -0,0 +1,16 @@ + + + + + + + diff --git a/steps/unit-tests-solution/src/environments/environment.prod.ts b/steps/unit-tests-solution/src/environments/environment.prod.ts new file mode 100644 index 00000000..051ee330 --- /dev/null +++ b/steps/unit-tests-solution/src/environments/environment.prod.ts @@ -0,0 +1,13 @@ +export const environment = { + production: true, + backend: { + protocol: 'http', + host: '127.0.0.1', + port: '9000', + endpoints: { + allPeople: '/api/peoples', + onePeople: '/api/peoples/:id', + randomPeople: '/api/peoples/random' + } + } +}; diff --git a/steps/unit-tests-solution/src/environments/environment.ts b/steps/unit-tests-solution/src/environments/environment.ts new file mode 100644 index 00000000..0a0c15f8 --- /dev/null +++ b/steps/unit-tests-solution/src/environments/environment.ts @@ -0,0 +1,18 @@ +// The file contents for the current environment will overwrite these during build. +// The build system defaults to the dev environment which uses `environment.ts`, but if you do +// `ng build --env=prod` then `environment.prod.ts` will be used instead. +// The list of which env maps to which file can be found in `angular-cli.json`. + +export const environment = { + production: false, + backend: { + protocol: 'http', + host: '127.0.0.1', + port: '9000', + endpoints: { + allPeople: '/api/peoples', + onePeople: '/api/peoples/:id', + randomPeople: '/api/peoples/random' + } + } +}; diff --git a/steps/unit-tests-solution/src/favicon.ico b/steps/unit-tests-solution/src/favicon.ico new file mode 100644 index 00000000..8081c7ce Binary files /dev/null and b/steps/unit-tests-solution/src/favicon.ico differ diff --git a/steps/unit-tests-solution/src/index.html b/steps/unit-tests-solution/src/index.html new file mode 100644 index 00000000..417c1531 --- /dev/null +++ b/steps/unit-tests-solution/src/index.html @@ -0,0 +1,21 @@ + + + + + Angular2200 + + + + + + + + +
+ + + +
+
+ + diff --git a/steps/unit-tests-solution/src/main.ts b/steps/unit-tests-solution/src/main.ts new file mode 100644 index 00000000..a9ca1caf --- /dev/null +++ b/steps/unit-tests-solution/src/main.ts @@ -0,0 +1,11 @@ +import { enableProdMode } from '@angular/core'; +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; + +import { AppModule } from './app/app.module'; +import { environment } from './environments/environment'; + +if (environment.production) { + enableProdMode(); +} + +platformBrowserDynamic().bootstrapModule(AppModule); diff --git a/steps/unit-tests-solution/src/polyfills.ts b/steps/unit-tests-solution/src/polyfills.ts new file mode 100644 index 00000000..0257e963 --- /dev/null +++ b/steps/unit-tests-solution/src/polyfills.ts @@ -0,0 +1,62 @@ +/** + * 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/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 to support `@angular/animation`. */ +// import 'web-animations-js'; // Run `npm install --save web-animations-js`. + +/** Evergreen browsers require these. **/ +import 'core-js/es6/reflect'; +import 'core-js/es7/reflect'; + +/** ALL Firefox browsers require the following to support `@angular/animation`. **/ +// import 'web-animations-js'; // Run `npm install --save web-animations-js`. + +/*************************************************************************************************** + * Zone JS is required by Angular itself. + */ +import 'zone.js/dist/zone'; // Included with Angular CLI. + +/*************************************************************************************************** + * APPLICATION IMPORTS + */ + +/** + * Date, currency, decimal and percent pipes. + * Needed for: All but Chrome, Firefox, Edge, IE11 and Safari 10 + */ +// import 'intl'; // Run `npm install --save intl`. diff --git a/steps/unit-tests-solution/src/styles.css b/steps/unit-tests-solution/src/styles.css new file mode 100644 index 00000000..57faf2d8 --- /dev/null +++ b/steps/unit-tests-solution/src/styles.css @@ -0,0 +1,103 @@ +@import '~@angular/material/prebuilt-themes/indigo-pink.css'; + +body { + background: red; + padding: 0; + margin: 0; + font-family: 'Open Sans', sans-serif; + background-color: #fafafa; + -webkit-font-smoothing: antialiased; +} +* /deep/ * { + font-family: inherit; + font-size: 14px; + line-height: 1.42857143; + color: inherit; +} +h1, +* /deep/ h1 { + font-size: 36px; + margin-top: 20px; + margin-bottom: 10px; +} + +button[mat-fab], +a[mat-fab] { + position: fixed; + bottom: 20px; + right: 20px; +} + +[mat-fab] mat-icon { + font-size: 2em; + line-height: 18.6px; +} + +.loader { + position: relative; + margin: 0 auto; + width: 100px; + transform: scale(1.5); +} +.loader:before { + content: ''; + display: block; + padding-top: 100%; +} + +.circular { + animation: rotate 2s linear infinite; + height: 100%; + transform-origin: center center; + width: 100%; + position: absolute; + top: 0; + bottom: 0; + left: 0; + right: 0; + margin: auto; +} + +.path { + stroke-dasharray: 1, 200; + stroke-dashoffset: 0; + animation: dash 1.5s ease-in-out infinite, color 6s ease-in-out infinite; + stroke-linecap: round; +} + +@keyframes rotate { + 100% { + -webkit-transform: rotate(360deg); + transform: rotate(360deg); + } +} +@keyframes dash { + 0% { + stroke-dasharray: 1, 200; + stroke-dashoffset: 0; + } + 50% { + stroke-dasharray: 89, 200; + stroke-dashoffset: -35px; + } + 100% { + stroke-dasharray: 89, 200; + stroke-dashoffset: -124px; + } +} +@keyframes color { + 100%, + 0% { + stroke: #d62d20; + } + 40% { + stroke: #0057e7; + } + 66% { + stroke: #008744; + } + 80%, + 90% { + stroke: #ffa700; + } +} diff --git a/steps/unit-tests-solution/src/test.ts b/steps/unit-tests-solution/src/test.ts new file mode 100644 index 00000000..4d72f17a --- /dev/null +++ b/steps/unit-tests-solution/src/test.ts @@ -0,0 +1,26 @@ +// This file is required by karma.conf.js and loads recursively all the .spec and framework files + +import 'zone.js/dist/long-stack-trace-zone'; +import 'zone.js/dist/proxy.js'; +import 'zone.js/dist/sync-test'; +import 'zone.js/dist/jasmine-patch'; +import 'zone.js/dist/async-test'; +import 'zone.js/dist/fake-async-test'; +import { getTestBed } from '@angular/core/testing'; +import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing'; + +// Unfortunately there's no typing for the `__karma__` variable. Just declare it as any. +declare var __karma__: any; +declare var require: any; + +// Prevent Karma from running prematurely. +__karma__.loaded = function() {}; + +// 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); +// Finally, start Karma to run the tests. +__karma__.start(); diff --git a/steps/unit-tests-solution/src/typings.d.ts b/steps/unit-tests-solution/src/typings.d.ts new file mode 100644 index 00000000..a73f5867 --- /dev/null +++ b/steps/unit-tests-solution/src/typings.d.ts @@ -0,0 +1,5 @@ +// Typings reference file, see links for more information +// https://github.com/typings/typings +// https://www.typescriptlang.org/docs/handbook/writing-declaration-files.html + +declare var System: any; diff --git a/steps/unit-tests-solution/tsconfig.app.json b/steps/unit-tests-solution/tsconfig.app.json new file mode 100644 index 00000000..7beda31d --- /dev/null +++ b/steps/unit-tests-solution/tsconfig.app.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "../../out-tsc/app", + "types": [] + }, + "exclude": ["test.ts", "**/*.spec.ts"] +} diff --git a/steps/unit-tests-solution/tsconfig.spec.json b/steps/unit-tests-solution/tsconfig.spec.json new file mode 100644 index 00000000..143838d9 --- /dev/null +++ b/steps/unit-tests-solution/tsconfig.spec.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "../../out-tsc/spec", + "types": ["jasmine", "node"] + }, + "files": ["src/test.ts", "src/polyfills.ts"], + "include": ["**/*.spec.ts", "**/*.d.ts"] +} diff --git a/steps/unit-tests-solution/tslint.json b/steps/unit-tests-solution/tslint.json new file mode 100644 index 00000000..8006e74e --- /dev/null +++ b/steps/unit-tests-solution/tslint.json @@ -0,0 +1,7 @@ +{ + "extends": "../../tslint.json", + "rules": { + "directive-selector": [true, "attribute", "app", "camelCase"], + "component-selector": [true, "element", "app", "kebab-case"] + } +}