diff --git a/README.md b/README.md index 0eb18de..0c74b7a 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,10 @@ -# An opinionated Angular 2 + Webpack boilerplate +# An Angular 2 + Webpack boilerplate with examples -**Work in progress.** This kit is not created for learning purposes, but rather as a base for my own upcoming projects. I'm not sure that world needs _yet another_ Angular 2 + webpack boilerplate, but initially it was shared as a solution for [TypeScript code coverage issue](https://github.com/AngularClass/angular2-webpack-starter/issues/178). +Angular 2 application with some examples (currently only HTTP service and component). Please, feel free to create issues and PRs. -Feel free to create issues and PRs, though, if you know how to do this thing better. +Templates and stylesheets are embedded into JS bundle with help of [angular2-template-loader](https://github.com/TheLarkInn/angular2-template-loader). SASS/SCSS is used for styling. -### What's inside - -Angular 2 example application (examples for components, HTTP, forms, routing are yet to come - most likely when Angular 2 is out of RC state). - -Templates and stylesheets are embedded into JS bundle. I chose SASS for styling. - -`index.html` is generated using [html-webpack-plugin](https://github.com/ampedandwired/html-webpack-plugin). Some popular analytics/metrics are yet to be added there (e.g. Google Analytics or New Relic). +`index.html` is generated using [html-webpack-plugin](https://github.com/ampedandwired/html-webpack-plugin). Hot module replacement is not provided here. Possibly I'll add it later. Same goes for service workers. @@ -30,14 +24,30 @@ You won't find end-to-end tests in this project (usually people use Protractor f ### Building bundle(s) -Use `npm run build` and you will get JS bundles, their maps and `index.html` in `dist` directory. To build for production, use `NODE_ENV=production npm run build` (webpack configuration is chosen according to this environment variable). +Use `npm run build` and you will get JS bundles, their maps and `index.html` in `dist` directory. + +To build for production, use `NODE_ENV=production npm run build` (webpack configuration is chosen according to this environment variable). ### Documentation -Run `npm run docs` to generate documentation for TypeScript ([typedoc](https://github.com/TypeStrong/typedoc) is used for that) and SASS stylesheets (done with [kss-node](https://github.com/kss-node/kss-node)). This might seem a little bit unusual to have docs for styles, but I find KSS very nice tool to keep them understandable. +Run `npm run docs` to generate documentation for TypeScript ([typedoc](https://github.com/TypeStrong/typedoc) is used for that) and SASS stylesheets (done with [kss-node](https://github.com/kss-node/kss-node)). + +### Dev notes + +- Webpack is configured to resolve modules not only from `node_modules` directory, but also from `src`. This is done to keep imports simple and avoid situations like `import { MyService } from '../../../app.service'`. Especially important for tests, since they are in a separate directory. If you are using JetBrains IDE, mark `src` directory as resource root + +- Upgrade to newer version of `istanbul-instrumenter-loader` (`0.2.0` to `1.0.0`) breaks code coverage + +- Upgrade to `awesome-typescript-loader@2.*.*` is not possible, as it is targeted to work with webpack 2, which is yet in beta and I don't want to use it until it's released -### Notes for development +## TODOs -Modules are resolved not only from `node_modules` directory, but from `src` as well. This is done to keep imports simple and avoid situations like `import { MyService } from '../../../app.service'`. +- Examples + - Typeahead with throttle/distinct + - Pipe + - AuthGuard in router + - Forms + - Input and Output + - rxjs-based WebSocket -If you are using JetBrains IDE, mark `src` directory as resource root. +- Comments on tests diff --git a/package.json b/package.json index 4d3d2e3..b68a5c6 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,7 @@ "angular2", "webpack", "typescript", + "rxjs", "boilerplate", "starter" ], @@ -27,6 +28,7 @@ "coverage:remap": "remap-istanbul -i coverage/coverage.json -o coverage/coverage.json -t json -e node_modules,tests,karma.entry.ts", "coverage:report": "istanbul report", "build": "webpack", + "preinstall": "npm run clean", "postinstall": "typings install", "docs": "npm run docs:typedoc && npm run docs:kss", "docs:typedoc": "typedoc --options ./typedoc.json ./src/app", @@ -34,55 +36,55 @@ "start": "webpack-dev-server --inline --progress --profile --colors --watch --display-error-details --display-cached --content-base ./dist" }, "devDependencies": { - "@angular/common": "2.0.0", - "@angular/compiler": "2.0.0", - "@angular/core": "2.0.0", - "@angular/forms": "2.0.0", - "@angular/http": "2.0.0", - "@angular/platform-browser": "2.0.0", - "@angular/platform-browser-dynamic": "2.0.0", - "@angular/router": "3.0.0", - "awesome-typescript-loader": "^1.1.1", - "codelyzer": "^0.0.26", - "core-js": "^2.4.1", - "css-loader": "^0.23.1", - "es6-shim": "^0.35.1", - "file-loader": "^0.8.5", - "html-loader": "^0.4.3", - "html-webpack-plugin": "^2.22.0", - "istanbul-instrumenter-loader": "^0.2.0", - "jasmine-core": "^2.4.1", - "json-loader": "^0.5.4", - "karma": "^0.13.22", - "karma-coverage": "^1.1.1", - "karma-jasmine": "^1.0.2", - "karma-mocha-reporter": "^2.0.5", - "karma-phantomjs-launcher": "^1.0.1", - "karma-source-map-support": "^1.1.0", - "karma-sourcemap-loader": "^0.3.7", - "karma-webpack": "^1.7.0", - "kss": "^3.0.0-beta.14", - "node-sass": "^3.8.0", - "phantomjs-prebuilt": "^2.1.8", - "raw-loader": "^0.5.1", - "reflect-metadata": "^0.1.3", - "remap-istanbul": "^0.6.4", - "rimraf": "^2.5.2", + "@angular/common": "2.1.2", + "@angular/compiler": "2.1.2", + "@angular/core": "2.1.2", + "@angular/forms": "2.1.2", + "@angular/http": "2.1.2", + "@angular/platform-browser": "2.1.2", + "@angular/platform-browser-dynamic": "2.1.2", + "@angular/router": "3.1.2", + "angular-in-memory-web-api": "^0.1.14", + "angular2-template-loader": "0.6.0", + "awesome-typescript-loader": "1.1.1", + "codelyzer": "0.0.26", + "core-js": "2.4.1", + "es6-shim": "0.35.1", + "flexboxgrid-sass": "8.0.5", + "html-loader": "0.4.4", + "html-webpack-plugin": "2.24.1", + "istanbul-instrumenter-loader": "0.2.0", + "jasmine-core": "2.5.2", + "json-loader": "0.5.4", + "karma": "1.3.0", + "karma-coverage": "1.1.1", + "karma-jasmine": "1.0.2", + "karma-mocha-reporter": "2.2.0", + "karma-phantomjs-launcher": "1.0.2", + "karma-source-map-support": "1.2.0", + "karma-sourcemap-loader": "0.3.7", + "karma-webpack": "1.8.0", + "kss": "3.0.0-beta.16", + "node-sass": "3.11.2", + "normalize-scss": "6.0.0", + "phantomjs-prebuilt": "2.1.13", + "raw-loader": "0.5.1", + "reflect-metadata": "0.1.8", + "remap-istanbul": "0.7.0", + "rimraf": "2.5.4", "rxjs": "5.0.0-beta.12", - "sass-lint": "^1.8.2", - "sass-loader": "^3.2.3", - "source-map-loader": "^0.1.5", - "style-loader": "^0.13.1", - "to-string-loader": "^1.1.4", - "ts-helpers": "^1.1.1", - "tslint": "^3.14.0", - "tslint-loader": "^2.1.5", - "typedoc": "^0.4.4", - "typescript": "^2.0.2", - "typings": "^1.3.2", - "webpack": "^1.13.1", - "webpack-dev-server": "^1.14.1", + "sass-lint": "1.10.2", + "sass-loader": "4.0.2", + "skeleton-scss": "2.0.4", + "source-map-loader": "0.1.5", + "ts-helpers": "1.1.2", + "tslint": "3.15.1", + "typedoc": "0.5.1", + "typescript": "2.0.8", + "typings": "1.3.2", + "webpack": "1.13.3", + "webpack-dev-server": "1.16.2", "webpack-md5-hash": "0.0.5", - "zone.js": "^0.6.23" + "zone.js": "0.6.26" } } diff --git a/src/app/_variables.scss b/src/app/_variables.scss new file mode 100644 index 0000000..c44f46f --- /dev/null +++ b/src/app/_variables.scss @@ -0,0 +1,2 @@ +$heading-color: #333; +$heading-highlight-color: #fa2800; diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 3a11545..75ea701 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -1,19 +1,14 @@ import { Component, ViewEncapsulation } from '@angular/core'; -import { AppService } from './app.service'; +/** + * Styles required here are common for all components (SASS/SCSS versions of normalize.css and flexboxgrid), + * so encapsulation is not used. Other components have their styles scoped with `ViewEncapsulation.Emulated`. + */ @Component({ selector: 'da-app', - template: require('./app.html'), - styles: [ - require('./app.scss') - ], - encapsulation: ViewEncapsulation.Native, - providers: [ - AppService - ] + templateUrl: './app.html', + styleUrls: ['./app.scss'], + encapsulation: ViewEncapsulation.None, + providers: [] }) -export class AppComponent { - constructor(public appService: AppService) { - - } -} +export class AppComponent {} diff --git a/src/app/app.html b/src/app/app.html index 4658077..3acd108 100644 --- a/src/app/app.html +++ b/src/app/app.html @@ -1 +1,17 @@ -

{{ appService.getTitle() }}

+
+
+
+

Example Angular 2 application

+
+
+ + + +
+ + +
diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 85aa374..4e8ee1d 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -1,14 +1,28 @@ import { NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; +import { HttpModule, JsonpModule } from '@angular/http'; +import { FormsModule } from '@angular/forms'; + +import { routing, appRoutingProviders } from './app.routing'; import { AppComponent } from 'app/app.component'; +import { ArticleComponent } from './article/article.component'; +import { ArticleListComponent } from './article/article-list.component'; -/** - * TODO: Example service (HTTP interaction), routing, nested directives, pipe + related tests - */ @NgModule({ - imports: [ BrowserModule ], - declarations: [ AppComponent ], - bootstrap: [ AppComponent ] + imports: [ + BrowserModule, + FormsModule, + HttpModule, + JsonpModule, + routing + ], + declarations: [ + AppComponent, + ArticleListComponent, + ArticleComponent + ], + providers: [appRoutingProviders], + bootstrap: [AppComponent] }) -export class AppModule { } +export class AppModule {} diff --git a/src/app/app.routing.ts b/src/app/app.routing.ts new file mode 100644 index 0000000..6bf8ed8 --- /dev/null +++ b/src/app/app.routing.ts @@ -0,0 +1,14 @@ +import { ModuleWithProviders } from '@angular/core'; +import { Routes, RouterModule } from '@angular/router'; + +import { ArticleComponent } from './article/article.component'; +import { ArticleListComponent } from './article/article-list.component'; + +const appRoutes: Routes = [ + { path: 'articles/:id', component: ArticleComponent }, + { path: '', component: ArticleListComponent }, +]; + +export const appRoutingProviders: any[] = []; + +export const routing: ModuleWithProviders = RouterModule.forRoot(appRoutes); diff --git a/src/app/app.scss b/src/app/app.scss index c0d5796..0ec6b65 100644 --- a/src/app/app.scss +++ b/src/app/app.scss @@ -1,16 +1,2 @@ -$heading-color: #333; -$heading-highlight-color: #fa2800; - -// App title -// -// :hover - Highlights when hovering -// -// Styleguide base -.app__title { - font-size: 4rem; - color: $heading-color; - - &:hover { - color: $heading-highlight-color; - } -} +@import "~normalize-scss/sass/normalize"; +@import "~flexboxgrid-sass"; diff --git a/src/app/app.service.ts b/src/app/app.service.ts deleted file mode 100644 index f4c9acb..0000000 --- a/src/app/app.service.ts +++ /dev/null @@ -1,7 +0,0 @@ -export class AppService { - title: string = "Hello, world!"; - - public getTitle(): string { - return this.title; - } -} diff --git a/src/app/article/article-list.component.ts b/src/app/article/article-list.component.ts new file mode 100644 index 0000000..938e2da --- /dev/null +++ b/src/app/article/article-list.component.ts @@ -0,0 +1,33 @@ +import { Component, ViewEncapsulation, OnInit, OnDestroy } from '@angular/core'; + +import { ArticleService } from './article.service'; +import { Article } from './article.model'; +import { Response } from '@angular/http'; + +/** + * A simple component, which fetches articles list from HTTP API and displays them. + */ +@Component({ + selector: 'da-article-list', + templateUrl: './article-list.html', + styleUrls: ['./article-list.scss'], + encapsulation: ViewEncapsulation.Emulated, + providers: [ArticleService] +}) +export class ArticleListComponent implements OnInit, OnDestroy { + private articles: Article[]; + private error: Response; + private isLoading: boolean = true; + + constructor(private articleService: ArticleService) {} + + ngOnInit(): void { + this.articleService.getAll().subscribe( + (data) => this.articles = data, + (error) => this.error = error, + () => this.isLoading = false + ); + } + + ngOnDestroy(): void {} +} diff --git a/src/app/article/article-list.html b/src/app/article/article-list.html new file mode 100644 index 0000000..314d71f --- /dev/null +++ b/src/app/article/article-list.html @@ -0,0 +1,7 @@ +
+ Loading… +
+ +
+

{{ article.title }}

+
\ No newline at end of file diff --git a/src/app/article/article-list.scss b/src/app/article/article-list.scss new file mode 100644 index 0000000..e69de29 diff --git a/src/app/article/article.component.ts b/src/app/article/article.component.ts new file mode 100644 index 0000000..79906c6 --- /dev/null +++ b/src/app/article/article.component.ts @@ -0,0 +1,43 @@ +import { Component, ViewEncapsulation, OnInit, OnDestroy } from '@angular/core'; +import { ActivatedRoute } from '@angular/router'; + +import { ArticleService } from './article.service'; +import { Article } from './article.model'; +import { Response } from '@angular/http'; + +/** + * A simple component, which fetches article from HTTP API and displays it. + */ +@Component({ + selector: 'da-article', + templateUrl: './article.html', + styleUrls: ['./article.scss'], + encapsulation: ViewEncapsulation.Emulated, + providers: [ArticleService] +}) +export class ArticleComponent implements OnInit, OnDestroy { + private article: Article; + private error: Response; + private isLoading: boolean = true; + + constructor( + private route: ActivatedRoute, + private articleService: ArticleService + ) { + + } + + /** + * TODO: Note about non-observable param + */ + ngOnInit(): void { + let id = +this.route.snapshot.params['id']; + this.articleService.get(id).subscribe( + (data) => this.article = data, + (error) => this.error = error, + () => this.isLoading = false + ); + } + + ngOnDestroy(): void {} +} diff --git a/src/app/article/article.html b/src/app/article/article.html new file mode 100644 index 0000000..7b7d1f5 --- /dev/null +++ b/src/app/article/article.html @@ -0,0 +1,19 @@ +
+ Loading… +
+ +
+ Something went wrong! +
+ +
+
+

{{ article?.title }}

+
+
+ {{ article?.body }} +
+ +
\ No newline at end of file diff --git a/src/app/article/article.model.ts b/src/app/article/article.model.ts new file mode 100644 index 0000000..e0c08fc --- /dev/null +++ b/src/app/article/article.model.ts @@ -0,0 +1,5 @@ +export class Article { + id: number; + title: string; + body: string; +} diff --git a/src/app/article/article.scss b/src/app/article/article.scss new file mode 100644 index 0000000..f5d329e --- /dev/null +++ b/src/app/article/article.scss @@ -0,0 +1,20 @@ +@import "../variables"; + +// An article detail view +// +// header - Primary info +// header h2 - Title +// header h2:hover - Title is highlighted when hovered +// +// Styleguide base +.article { + header { + h2 { + color: $heading-color; + + &:hover { + color: $heading-highlight-color; + } + } + } +} diff --git a/src/app/article/article.service.ts b/src/app/article/article.service.ts new file mode 100644 index 0000000..b504b3d --- /dev/null +++ b/src/app/article/article.service.ts @@ -0,0 +1,34 @@ +import { Injectable } from '@angular/core'; +import { Http, Response } from '@angular/http'; +import { Observable } from 'rxjs'; + +import { Article } from './article.model'; + +const API_ENDPOINT = 'https://jsonplaceholder.typicode.com/posts'; + +/** + * A simple service which fetches data from HTTP API. + * + * TODO: Move API endpoint to app config + */ +@Injectable() +export class ArticleService { + constructor(private http: Http) {} + + public get(id: number): Observable
{ + return this.http + .get(API_ENDPOINT + '/' + id) + .map(this.extractData); + } + + public getAll(): Observable { + return this.http + .get(API_ENDPOINT) + .map(this.extractData); + } + + private extractData(res: Response) { + let body = res.json(); + return body || { }; + } +} diff --git a/src/main.ts b/src/main.ts index dc1cc0b..61d69ad 100644 --- a/src/main.ts +++ b/src/main.ts @@ -3,7 +3,7 @@ import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; import { AppModule } from 'app/app.module'; -if (process.env.ENV === 'prod') { +if (['prod', 'production'].indexOf(process.env.ENV) != -1) { enableProdMode(); } diff --git a/src/polyfills.ts b/src/polyfills.ts index cc23c6f..5c1049f 100644 --- a/src/polyfills.ts +++ b/src/polyfills.ts @@ -17,7 +17,7 @@ import 'zone.js/dist/zone'; */ import 'ts-helpers'; -if (process.env.ENV !== 'production') { +if (['prod', 'production'].indexOf(process.env.ENV) == -1) { /** * You can use `import` only in a namespace or module in TypeScript, so use `require` here. */ diff --git a/src/vendor.ts b/src/vendor.ts index a8a9dfa..6c4db5c 100644 --- a/src/vendor.ts +++ b/src/vendor.ts @@ -5,6 +5,8 @@ import '@angular/platform-browser'; import '@angular/platform-browser-dynamic'; import '@angular/core'; import '@angular/common'; -import '@angular/http'; import '@angular/router'; +import '@angular/http'; +import '@angular/forms'; + import 'rxjs'; diff --git a/tests/activated-route-stub.ts b/tests/activated-route-stub.ts new file mode 100644 index 0000000..24bf806 --- /dev/null +++ b/tests/activated-route-stub.ts @@ -0,0 +1,22 @@ +import { BehaviorSubject } from 'rxjs'; +import { Injectable } from '@angular/core'; + +/** + * This stub was borrowed from official tutorial. + */ +@Injectable() +export class ActivatedRouteStub { + private subject = new BehaviorSubject(this.testParams); + params = this.subject.asObservable(); + + private _testParams: {}; + get testParams() { return this._testParams; } + set testParams(params: {}) { + this._testParams = params; + this.subject.next(params); + } + + get snapshot() { + return { params: this.testParams }; + } +} diff --git a/tests/app/app.component.spec.ts b/tests/app/app.component.spec.ts deleted file mode 100644 index fc5a102..0000000 --- a/tests/app/app.component.spec.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { TestBed, inject } from '@angular/core/testing'; -import { AppService } from 'app/app.service'; -import { AppComponent } from 'app/app.component'; - -describe('App Service', () => { - beforeEach(() => { - TestBed.configureTestingModule({ - declarations: [ AppComponent ], - providers: [ AppService ] - }); - }); - - it('should return title', inject([ AppService ], (appService: AppService) => { - expect(appService.getTitle()).toBe('Hello, world!'); - })); -}); diff --git a/tests/app/article/article-list.component.spec.ts b/tests/app/article/article-list.component.spec.ts new file mode 100644 index 0000000..06e3fbe --- /dev/null +++ b/tests/app/article/article-list.component.spec.ts @@ -0,0 +1,62 @@ +import { TestBed, async, fakeAsync, tick } from '@angular/core/testing'; +import { Route } from '@angular/router'; +import { HttpModule } from '@angular/http'; +import { RouterTestingModule } from '@angular/router/testing'; +import { Observable } from 'rxjs'; + +import { ArticleService } from 'app/article/article.service'; +import { ArticleListComponent } from 'app/article/article-list.component'; +import { ArticleComponent } from 'app/article/article.component'; +import { Article } from '../../../src/app/article/article.model'; + +const exampleArticleList: Article[] = [ + { + id: 1, + title: 'Hello, world', + body: 'Lorem ipsum dolor sit amet.', + userId: 1 + }, + { + id: 2, + title: 'Good bye, world', + body: 'Lorem ipsum dolor sit amet.', + userId: 1 + } +]; + +/** + * An example of component testing. + */ +describe('ArticleList Component', () => { + let routerConfig: Route[] = [ + { path: 'articles/:id', component: ArticleComponent } + ]; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [ + HttpModule, + RouterTestingModule.withRoutes(routerConfig) + ], + providers: [ArticleService], + declarations: [ArticleListComponent, ArticleComponent] + }).compileComponents(); + })); + + beforeEach(() => { + this.fixture = TestBed.createComponent(ArticleListComponent); + this.comp = this.fixture.componentInstance; + this.articleService = this.fixture.debugElement.injector.get(ArticleService); + this.getAllArticlesSpy = spyOn(this.articleService, 'getAll').and.returnValue(Observable.of(exampleArticleList)); + this.el = this.fixture.debugElement.nativeElement; + }); + + it('should get articles from service', fakeAsync(() => { + expect(this.getAllArticlesSpy).toHaveBeenCalledTimes(0); + this.fixture.detectChanges(); + expect(this.getAllArticlesSpy).toHaveBeenCalledTimes(1); + tick(); + this.fixture.detectChanges(); + expect(this.comp.articles).toEqual(exampleArticleList); + })); +}); diff --git a/tests/app/article/article.component.spec.ts b/tests/app/article/article.component.spec.ts new file mode 100644 index 0000000..2b7a445 --- /dev/null +++ b/tests/app/article/article.component.spec.ts @@ -0,0 +1,52 @@ +import { TestBed, async, fakeAsync, tick } from '@angular/core/testing'; +import { ActivatedRoute } from '@angular/router'; +import { HttpModule } from '@angular/http'; +import { Observable } from 'rxjs'; + +import { ArticleService } from 'app/article/article.service'; +import { ArticleComponent } from 'app/article/article.component'; +import { Article } from 'app/article/article.model'; +import { ActivatedRouteStub } from '../../activated-route-stub'; + +const exampleArticle: Article = { + id: 2, + title: 'Good bye, world', + body: 'Lorem ipsum dolor sit amet.', + userId: 1 +}; + +/** + * An example of component testing. + */ +describe('Article Component', () => { + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [ + HttpModule + ], + providers: [ + ArticleService, + { provide: ActivatedRoute, useClass: ActivatedRouteStub } + ], + declarations: [ArticleComponent] + }).compileComponents(); + })); + + beforeEach(() => { + this.fixture = TestBed.createComponent(ArticleComponent); + this.comp = this.fixture.componentInstance; + this.articleService = this.fixture.debugElement.injector.get(ArticleService); + this.getArticleSpy = spyOn(this.articleService, 'get').and.returnValue(Observable.of(exampleArticle)); + this.el = this.fixture.debugElement.nativeElement; + this.fixture.debugElement.injector.get(ActivatedRoute).testParams = { id: exampleArticle.id } + }); + + it('should get article from service', fakeAsync(() => { + expect(this.getArticleSpy).toHaveBeenCalledTimes(0); + this.fixture.detectChanges(); + expect(this.getArticleSpy).toHaveBeenCalledWith(exampleArticle.id); + tick(); + this.fixture.detectChanges(); + expect(this.comp.article).toEqual(exampleArticle); + })); +}); diff --git a/tests/app/article/article.service.spec.ts b/tests/app/article/article.service.spec.ts new file mode 100644 index 0000000..654b0d6 --- /dev/null +++ b/tests/app/article/article.service.spec.ts @@ -0,0 +1,62 @@ +import { TestBed, inject } from '@angular/core/testing'; +import { BaseRequestOptions, Response, ResponseOptions, Http } from '@angular/http'; +import { MockBackend, MockConnection } from '@angular/http/testing'; + +import { ArticleService } from 'app/article/article.service'; +import { Article } from 'app/article/article.model'; + +const exampleArticleList: Article[] = [ + { + id: 1, + title: 'Hello, world', + body: 'Lorem ipsum dolor sit amet.', + userId: 1 + } +]; + +/** + * An example of service testing with HTTP response mocking. + */ +describe('Article Service', () => { + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [ + ArticleService, + BaseRequestOptions, + MockBackend, + { + provide: Http, + useFactory: (backend: MockBackend, defaultOptions: BaseRequestOptions) => { + return new Http(backend, defaultOptions) + }, + deps: [MockBackend, BaseRequestOptions] + } + ] + }); + }); + + it('should fetch single article', inject([ArticleService, MockBackend], (articleService: ArticleService, backend: MockBackend) => { + const baseResponse = new Response(new ResponseOptions({ + body: JSON.stringify(exampleArticleList[0]) + })); + + backend.connections.subscribe((c: MockConnection) => { + c.mockRespond(baseResponse); + }); + + articleService.get(1).subscribe((data) => { + expect(data).toEqual(exampleArticleList[0]); + }); + })); + + it('should fetch multiple articles', inject([ArticleService, MockBackend], (articleService: ArticleService, backend: MockBackend) => { + const baseResponse = new Response(new ResponseOptions({ + body: JSON.stringify(exampleArticleList) + })); + backend.connections.subscribe((c: MockConnection) => c.mockRespond(baseResponse)); + + articleService.getAll().subscribe((data) => { + expect(data).toEqual(exampleArticleList); + }); + })); +}); diff --git a/tsconfig.json b/tsconfig.json index cd93acf..58d4ded 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -20,7 +20,8 @@ ], "awesomeTypescriptLoaderOptions": { "resolveGlobs": true, - "forkChecker": true + "forkChecker": true, + "useWebpackText": true }, "compileOnSave": false, "buildOnSave": false diff --git a/typings.json b/typings.json index 8e56abc..2317d8c 100644 --- a/typings.json +++ b/typings.json @@ -1,8 +1,8 @@ { "name": "angular2-webpack-boilerplate", "globalDependencies": { - "core-js": "registry:dt/core-js#0.0.0+20160602141332", - "jasmine": "registry:dt/jasmine#2.2.0+20160621224255", - "node": "registry:dt/node#6.0.0+20160621231320" + "core-js": "registry:dt/core-js#0.0.0+20160914114559", + "jasmine": "registry:dt/jasmine#2.5.0+20161003201800", + "node": "registry:dt/node#6.0.0+20161019125345" } } \ No newline at end of file diff --git a/webpack.config.common.js b/webpack.config.common.js index f25983e..6b4816e 100644 --- a/webpack.config.common.js +++ b/webpack.config.common.js @@ -25,19 +25,27 @@ module.exports = { resolve: { extensions: ['', '.ts', '.js', '.scss', '.html'], + /** + * Adding src to resolving paths allows us to do, say, `import 'app/a/a.service'` instead of + * import `../../../a/service`. Especially useful for tests, since they are in a separate directory. + */ modulesDirectories: ['node_modules', 'src'] }, module: { loaders: [ /** - * Loader for TypeScript. + * Loaders for TypeScript. * No need to exclude tests by `(spec|e2e)` mask here, as they are in separate directory. * * See project repository for details / configuration reference: * https://github.com/s-panferov/awesome-typescript-loader + * https://github.com/TheLarkInn/angular2-template-loader */ - {test: /\.ts$/, loader: 'awesome-typescript-loader'}, + { + test: /\.ts$/, + loaders: ['awesome-typescript-loader', 'angular2-template-loader'] + }, /** * Loaders for HTML templates, JSON files, SASS/SCSS stylesheets. See details at projects' repositories: @@ -47,8 +55,8 @@ module.exports = { * https://github.com/gajus/to-string-loader */ {test: /\.json$/, loader: 'json-loader'}, - {test: /\.html$/, loader: 'html-loader'}, - {test: /\.scss$/, loader: 'to-string-loader!css-loader!sass-loader'} + {test: /\.html$/, loader: 'raw-loader'}, + {test: /\.scss$/, loaders: ['raw-loader', 'sass-loader']} ] }, diff --git a/webpack.config.prod.js b/webpack.config.prod.js index 3977520..103321d 100644 --- a/webpack.config.prod.js +++ b/webpack.config.prod.js @@ -17,21 +17,22 @@ config.output = { sourceMapFilename: '[name].[hash].map' }; -/** - * TODO: Remove comments from resulting JS files - */ - -/** - * TODO: Something better? - */ -config.htmlLoader = {minimize: false}; - config.plugins.push( new webpack.NoErrorsPlugin(), new webpack.optimize.DedupePlugin(), - new webpack.optimize.UglifyJsPlugin() + new webpack.optimize.UglifyJsPlugin({ + comments: false + }) ); +/** + * See node-sass documentation for full list of options: + * https://github.com/sass/node-sass#options + */ +config.sassLoader = { + outputStyle: 'compressed' +}; + /** * Same purpose as in dev config. */ diff --git a/webpack.config.test.js b/webpack.config.test.js index d22f431..bce802b 100644 --- a/webpack.config.test.js +++ b/webpack.config.test.js @@ -21,23 +21,24 @@ module.exports = { /** * Enable inline source maps for code coverage report. * - * See project repository for details / configuration reference: - * https://github.com/s-panferov/awesome-typescript-loader + * See info about loaders in dev config. */ { test: /\.ts$/, - loader: 'awesome-typescript-loader', - query: { - sourceMap: false, - inlineSourceMap: true - } + loaders: [ + 'awesome-typescript-loader?' + JSON.stringify({ + sourceMap: false, + inlineSourceMap: true + }), + 'angular2-template-loader' + ] }, /** - * These loaders are used in other environments as well. + * These loaders are used in other environments as well, see `webpack.config.common.js`. */ {test: /\.json$/, loader: 'json-loader'}, - {test: /\.html$/, loader: 'html-loader'}, - {test: /\.scss$/, loaders: ['to-string-loader', 'css-loader', 'sass-loader']} + {test: /\.html$/, loader: 'raw-loader'}, + {test: /\.scss$/, loaders: ['raw-loader', 'sass-loader']} ], postLoaders: [ /**