├── .editorconfig ├── .gitignore ├── .travis.yml ├── .vscode └── launch.json ├── README.md ├── angular.json ├── e2e ├── protractor.conf.js ├── src │ ├── app.e2e-spec.ts │ └── app.po.ts └── tsconfig.e2e.json ├── jest.config.js ├── package-lock.json ├── package.json ├── src ├── app │ ├── app-routing.module.ts │ ├── app.component.html │ ├── app.component.scss │ ├── app.component.spec.ts │ ├── app.component.ts │ ├── app.module.ts │ ├── core │ │ ├── core.module.spec.ts │ │ ├── core.module.ts │ │ ├── db │ │ │ └── root.db.service.ts │ │ └── services │ │ │ ├── user.service.spec.ts │ │ │ └── user.service.ts │ ├── state │ │ ├── app.effects.spec.ts │ │ ├── app.effects.ts │ │ ├── index.ts │ │ ├── shared │ │ │ └── utils.ts │ │ ├── state.module.spec.ts │ │ ├── state.module.ts │ │ └── user │ │ │ ├── index.spec.ts │ │ │ ├── index.ts │ │ │ ├── user.actions.ts │ │ │ ├── user.effects.spec.ts │ │ │ ├── user.effects.ts │ │ │ ├── user.model.ts │ │ │ ├── user.reducer.spec.ts │ │ │ └── user.reducer.ts │ └── users │ │ ├── components │ │ ├── user-form │ │ │ ├── __snapshots__ │ │ │ │ └── user-form.component.spec.ts.snap │ │ │ ├── user-form.component.html │ │ │ ├── user-form.component.scss │ │ │ ├── user-form.component.spec.ts │ │ │ └── user-form.component.ts │ │ └── user-list │ │ │ ├── user-list.component.html │ │ │ ├── user-list.component.scss │ │ │ ├── user-list.component.spec.ts │ │ │ └── user-list.component.ts │ │ ├── containers │ │ ├── add │ │ │ ├── add.component.html │ │ │ ├── add.component.scss │ │ │ ├── add.component.spec.ts │ │ │ └── add.component.ts │ │ ├── edit │ │ │ ├── __snapshots__ │ │ │ │ └── edit.component.spec.ts.snap │ │ │ ├── edit.component.html │ │ │ ├── edit.component.scss │ │ │ ├── edit.component.spec.ts │ │ │ └── edit.component.ts │ │ └── index │ │ │ ├── __snapshots__ │ │ │ └── index.component.spec.ts.snap │ │ │ ├── index.component.html │ │ │ ├── index.component.scss │ │ │ ├── index.component.spec.ts │ │ │ └── index.component.ts │ │ ├── routes.ts │ │ ├── users.module.spec.ts │ │ └── users.module.ts ├── assets │ └── .gitkeep ├── browserslist ├── environments │ ├── environment.prod.ts │ └── environment.ts ├── favicon.ico ├── index.html ├── jestGlobalMocks.ts ├── main.ts ├── polyfills.ts ├── setup-jest.ts ├── setupJest.ts ├── styles.scss ├── tsconfig.app.json ├── tsconfig.spec.json └── tslint.json ├── tsconfig.json └── tslint.json /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | max_line_length = off 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist 5 | /tmp 6 | /out-tsc 7 | 8 | # dependencies 9 | /node_modules 10 | 11 | # IDEs and editors 12 | /.idea 13 | .project 14 | .classpath 15 | .c9/ 16 | *.launch 17 | .settings/ 18 | *.sublime-workspace 19 | 20 | # IDE - VSCode 21 | .vscode/* 22 | !.vscode/settings.json 23 | !.vscode/tasks.json 24 | !.vscode/launch.json 25 | !.vscode/extensions.json 26 | 27 | # misc 28 | /.sass-cache 29 | /connect.lock 30 | /coverage 31 | /libpeerconnection.log 32 | npm-debug.log 33 | yarn-error.log 34 | testem.log 35 | /typings 36 | 37 | # System Files 38 | .DS_Store 39 | Thumbs.db 40 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | dist: trusty 2 | sudo: false 3 | 4 | language: node_js 5 | node_js: 6 | - "8" 7 | 8 | cache: 9 | directories: 10 | - "node_modules" 11 | 12 | install: true 13 | 14 | script: 15 | - npm install 16 | - npm run lint 17 | - npm run test:ci 18 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "node", 9 | "request": "launch", 10 | "name": "Jest Test", 11 | "program": "${workspaceFolder}/node_modules/jest/bin/jest", 12 | "args": ["--runInBand"], 13 | "console": "integratedTerminal", 14 | "internalConsoleOptions": "neverOpen" 15 | }, 16 | { 17 | "name": "ng serve", 18 | "type": "chrome", 19 | "request": "launch", 20 | "url": "http://localhost:4200/", 21 | "webRoot": "${workspaceFolder}", 22 | "sourceMapPathOverrides": { 23 | "webpack:/./*": "${webRoot}/*", 24 | "webpack:/src/*": "${webRoot}/src/*", 25 | "webpack:/*": "*", 26 | "webpack:/./~/*": "${webRoot}/node_modules/*" 27 | } 28 | }, 29 | { 30 | "name": "ng e2e", 31 | "type": "node", 32 | "request": "launch", 33 | "program": "${workspaceFolder}/node_modules/protractor/bin/protractor", 34 | "protocol": "inspector", 35 | "args": ["${workspaceFolder}/protractor.conf.js"] 36 | } 37 | ] 38 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/blove/ngrx-testing.svg?branch=master)](https://travis-ci.org/blove/ngrx-testing) 2 | 3 | # NgRx Testing 4 | 5 | This project is based on a presentation given at the Rocky Mountain Angular meetup. 6 | Be sure to check out the [NgRx Testing slide deck](https://slides.com/blove/ngrx-testing-jasmine-marbles) as well. 7 | 8 | ## Blog Posts 9 | 10 | Check out some blog posts I wrote to get you started with Jest in an Angular project and testing NgRx apps using jasmine-marbles: 11 | 12 | * [Angular + Jest](https://brianflove.com/2018/05/26/angular-jest-testing/) 13 | * [NgRx: Testing Components](https://brianflove.com/2018/05/27/ngrx-testing-components/) 14 | * [NgRx: Testing Actions](https://brianflove.com/2018/05/28/ngrx-testing-actions/) 15 | * [NgRx: Testing Effects](https://brianflove.com/2018/06/28/ngrx-testing-effects/) 16 | 17 | ## Serve 18 | 19 | Start up the Angular CLI development server via: 20 | 21 | ```bash 22 | $ ng serve 23 | ``` 24 | 25 | ## Build 26 | 27 | Build the app via: 28 | 29 | ```bash 30 | $ ng build 31 | ``` 32 | 33 | ## Test 34 | 35 | This project uses Jest (instead of Karma) for running tests. 36 | Run the full test suite via: 37 | 38 | ```bash 39 | $ npm run test 40 | ``` 41 | 42 | You can also run the tests in a watch mode via: 43 | 44 | ```bash 45 | $ npm run test:watch 46 | ``` 47 | 48 | This project uses Jest snapshot testing. 49 | Update the snapshots via: 50 | 51 | ```bash 52 | $ npm run test -- --updateSnapshot 53 | ``` -------------------------------------------------------------------------------- /angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "version": 1, 4 | "newProjectRoot": "projects", 5 | "projects": { 6 | "ngrx-marbles": { 7 | "root": "", 8 | "sourceRoot": "src", 9 | "projectType": "application", 10 | "prefix": "app", 11 | "schematics": { 12 | "@schematics/angular:component": { 13 | "styleext": "scss" 14 | } 15 | }, 16 | "architect": { 17 | "build": { 18 | "builder": "@angular-devkit/build-angular:browser", 19 | "options": { 20 | "outputPath": "dist/ngrx-marbles", 21 | "index": "src/index.html", 22 | "main": "src/main.ts", 23 | "polyfills": "src/polyfills.ts", 24 | "tsConfig": "src/tsconfig.app.json", 25 | "assets": [ 26 | "src/favicon.ico", 27 | "src/assets" 28 | ], 29 | "styles": [ 30 | "src/styles.scss" 31 | ], 32 | "scripts": [] 33 | }, 34 | "configurations": { 35 | "production": { 36 | "fileReplacements": [ 37 | { 38 | "replace": "src/environments/environment.ts", 39 | "with": "src/environments/environment.prod.ts" 40 | } 41 | ], 42 | "optimization": true, 43 | "outputHashing": "all", 44 | "sourceMap": false, 45 | "extractCss": true, 46 | "namedChunks": false, 47 | "aot": true, 48 | "extractLicenses": true, 49 | "vendorChunk": false, 50 | "buildOptimizer": true 51 | } 52 | } 53 | }, 54 | "serve": { 55 | "builder": "@angular-devkit/build-angular:dev-server", 56 | "options": { 57 | "browserTarget": "ngrx-marbles:build" 58 | }, 59 | "configurations": { 60 | "production": { 61 | "browserTarget": "ngrx-marbles:build:production" 62 | } 63 | } 64 | }, 65 | "extract-i18n": { 66 | "builder": "@angular-devkit/build-angular:extract-i18n", 67 | "options": { 68 | "browserTarget": "ngrx-marbles:build" 69 | } 70 | }, 71 | "test": { 72 | "builder": "@angular-devkit/build-angular:karma", 73 | "options": { 74 | "main": "src/test.ts", 75 | "polyfills": "src/polyfills.ts", 76 | "tsConfig": "src/tsconfig.spec.json", 77 | "karmaConfig": "src/karma.conf.js", 78 | "styles": [ 79 | "styles.scss" 80 | ], 81 | "scripts": [], 82 | "assets": [ 83 | "src/favicon.ico", 84 | "src/assets" 85 | ] 86 | } 87 | }, 88 | "lint": { 89 | "builder": "@angular-devkit/build-angular:tslint", 90 | "options": { 91 | "tsConfig": [ 92 | "src/tsconfig.app.json", 93 | "src/tsconfig.spec.json" 94 | ], 95 | "exclude": [ 96 | "**/node_modules/**" 97 | ] 98 | } 99 | } 100 | } 101 | }, 102 | "ngrx-marbles-e2e": { 103 | "root": "e2e/", 104 | "projectType": "application", 105 | "architect": { 106 | "e2e": { 107 | "builder": "@angular-devkit/build-angular:protractor", 108 | "options": { 109 | "protractorConfig": "e2e/protractor.conf.js", 110 | "devServerTarget": "ngrx-marbles:serve" 111 | } 112 | }, 113 | "lint": { 114 | "builder": "@angular-devkit/build-angular:tslint", 115 | "options": { 116 | "tsConfig": "e2e/tsconfig.e2e.json", 117 | "exclude": [ 118 | "**/node_modules/**" 119 | ] 120 | } 121 | } 122 | } 123 | } 124 | }, 125 | "defaultProject": "ngrx-marbles", 126 | "cli": { 127 | "defaultCollection": "@ngrx/schematics" 128 | } 129 | } -------------------------------------------------------------------------------- /e2e/protractor.conf.js: -------------------------------------------------------------------------------- 1 | // Protractor configuration file, see link for more information 2 | // https://github.com/angular/protractor/blob/master/lib/config.ts 3 | 4 | const { SpecReporter } = require('jasmine-spec-reporter'); 5 | 6 | exports.config = { 7 | allScriptsTimeout: 11000, 8 | specs: [ 9 | './src/**/*.e2e-spec.ts' 10 | ], 11 | capabilities: { 12 | 'browserName': 'chrome' 13 | }, 14 | directConnect: true, 15 | baseUrl: 'http://localhost:4200/', 16 | framework: 'jasmine', 17 | jasmineNodeOpts: { 18 | showColors: true, 19 | defaultTimeoutInterval: 30000, 20 | print: function() {} 21 | }, 22 | onPrepare() { 23 | require('ts-node').register({ 24 | project: require('path').join(__dirname, './tsconfig.e2e.json') 25 | }); 26 | jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } })); 27 | } 28 | }; -------------------------------------------------------------------------------- /e2e/src/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { AppPage } from './app.po'; 2 | 3 | describe('workspace-project App', () => { 4 | let page: AppPage; 5 | 6 | beforeEach(() => { 7 | page = new AppPage(); 8 | }); 9 | 10 | it('should display welcome message', () => { 11 | page.navigateTo(); 12 | expect(page.getParagraphText()).toEqual('Welcome to app!'); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /e2e/src/app.po.ts: -------------------------------------------------------------------------------- 1 | import { browser, by, element } from 'protractor'; 2 | 3 | export class AppPage { 4 | navigateTo() { 5 | return browser.get('/'); 6 | } 7 | 8 | getParagraphText() { 9 | return element(by.css('app-root h1')).getText(); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /e2e/tsconfig.e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/app", 5 | "module": "commonjs", 6 | "target": "es5", 7 | "types": [ 8 | "jasmine", 9 | "jasminewd2", 10 | "node" 11 | ] 12 | } 13 | } -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'jest-preset-angular', 3 | setupTestFrameworkScriptFile: '/src/setup-jest.ts', 4 | moduleNameMapper: { 5 | '@core/(.*)': '/src/app/core/$1', 6 | '@state/(.*)': '/src/app/state/$1' 7 | } 8 | }; 9 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ngrx-marbles", 3 | "version": "0.0.0", 4 | "scripts": { 5 | "ng": "ng", 6 | "start": "ng serve", 7 | "build": "ng build", 8 | "test": "jest", 9 | "test:watch": "jest --watch", 10 | "test:ci": "jest --runInBand", 11 | "lint": "ng lint", 12 | "e2e": "ng e2e" 13 | }, 14 | "private": true, 15 | "dependencies": { 16 | "@angular/animations": "^6.0.7", 17 | "@angular/common": "^6.0.7", 18 | "@angular/compiler": "^6.0.7", 19 | "@angular/core": "^6.0.7", 20 | "@angular/forms": "^6.0.7", 21 | "@angular/http": "^6.0.7", 22 | "@angular/platform-browser": "^6.0.7", 23 | "@angular/platform-browser-dynamic": "^6.0.7", 24 | "@angular/router": "^6.0.7", 25 | "@ngrx/effects": "^6.0.1", 26 | "@ngrx/entity": "^6.0.1", 27 | "@ngrx/router-store": "^6.0.1", 28 | "@ngrx/store": "^6.0.1", 29 | "@ngrx/store-devtools": "^6.0.1", 30 | "angular-in-memory-web-api": "^0.6.0", 31 | "core-js": "^2.5.4", 32 | "rxjs": "^6.2.1", 33 | "zone.js": "^0.8.26" 34 | }, 35 | "devDependencies": { 36 | "@angular-devkit/build-angular": "^0.6.8", 37 | "@angular-devkit/core": "^0.6.8", 38 | "@angular-devkit/schematics": "^0.6.8", 39 | "@angular/cli": "^6.0.8", 40 | "@angular/compiler-cli": "^6.0.7", 41 | "@angular/language-service": "^6.0.7", 42 | "@ngrx/schematics": "^6.0.0-beta.0", 43 | "@types/faker": "^4.1.2", 44 | "@types/jest": "^22.2.3", 45 | "@types/node": "~10.5.2", 46 | "codelyzer": "~4.4.2", 47 | "faker": "^4.1.0", 48 | "jasmine-marbles": "^0.3.1", 49 | "jest": "^23.3.0", 50 | "jest-preset-angular": "^5.2.3", 51 | "ngrx-store-freeze": "^0.2.4", 52 | "protractor": "~5.3.0", 53 | "ts-node": "~7.0.0", 54 | "tslint": "~5.10.0", 55 | "typescript": "~2.7.2", 56 | "webpack-visualizer-plugin": "^0.1.11" 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/app/app-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { RouterModule, Routes } from '@angular/router'; 3 | import { userRoutes } from './users/routes'; 4 | 5 | const routes: Routes = [ 6 | { 7 | path: '', 8 | pathMatch: 'full', 9 | redirectTo: '/users' 10 | }, 11 | { 12 | path: 'users', 13 | children: userRoutes 14 | } 15 | ]; 16 | 17 | @NgModule({ 18 | imports: [RouterModule.forRoot(routes)], 19 | exports: [RouterModule] 20 | }) 21 | export class AppRoutingModule {} 22 | -------------------------------------------------------------------------------- /src/app/app.component.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/app.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blove/ngrx-testing/5449c76871375ead6be7df7f080bcebba54c5c7c/src/app/app.component.scss -------------------------------------------------------------------------------- /src/app/app.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed, async } from '@angular/core/testing'; 2 | import { RouterTestingModule } from '@angular/router/testing'; 3 | import { AppComponent } from './app.component'; 4 | 5 | describe('AppComponent', () => { 6 | beforeEach(async(() => { 7 | TestBed.configureTestingModule({ 8 | imports: [RouterTestingModule], 9 | declarations: [AppComponent] 10 | }).compileComponents(); 11 | })); 12 | 13 | it('should create the app', async(() => { 14 | const fixture = TestBed.createComponent(AppComponent); 15 | const app = fixture.debugElement.componentInstance; 16 | expect(app).toBeTruthy(); 17 | })); 18 | }); 19 | -------------------------------------------------------------------------------- /src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-root', 5 | templateUrl: './app.component.html', 6 | styleUrls: ['./app.component.scss'] 7 | }) 8 | export class AppComponent {} 9 | -------------------------------------------------------------------------------- /src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { BrowserModule } from '@angular/platform-browser'; 3 | import { CoreModule } from '@core/core.module'; 4 | import { StateModule } from '@state/state.module'; 5 | import { AppRoutingModule } from './app-routing.module'; 6 | import { AppComponent } from './app.component'; 7 | import { UsersModule } from './users/users.module'; 8 | 9 | @NgModule({ 10 | declarations: [AppComponent], 11 | imports: [ 12 | BrowserModule, 13 | AppRoutingModule, 14 | StateModule, 15 | CoreModule, 16 | UsersModule 17 | ], 18 | providers: [], 19 | bootstrap: [AppComponent] 20 | }) 21 | export class AppModule {} 22 | -------------------------------------------------------------------------------- /src/app/core/core.module.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | import { CoreModule } from '@core/core.module'; 3 | import { UserService } from './services/user.service'; 4 | 5 | describe(`CoreModule.forRoot()`, () => { 6 | beforeEach(() => { 7 | TestBed.configureTestingModule({ 8 | imports: [CoreModule.forRoot()] 9 | }); 10 | }); 11 | 12 | it(`should not provide 'UserService' service`, () => { 13 | expect(() => TestBed.get(UserService)).toBeTruthy(); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /src/app/core/core.module.ts: -------------------------------------------------------------------------------- 1 | import { CommonModule } from '@angular/common'; 2 | import { HttpClientModule } from '@angular/common/http'; 3 | import { 4 | ModuleWithProviders, 5 | NgModule, 6 | Optional, 7 | SkipSelf 8 | } from '@angular/core'; 9 | import { HttpClientInMemoryWebApiModule } from 'angular-in-memory-web-api'; 10 | import { RootDbService } from './db/root.db.service'; 11 | import { UserService } from './services/user.service'; 12 | 13 | @NgModule({ 14 | imports: [ 15 | CommonModule, 16 | HttpClientModule, 17 | HttpClientInMemoryWebApiModule.forRoot(RootDbService, { 18 | delay: 500 19 | }) 20 | ], 21 | declarations: [], 22 | providers: [UserService] 23 | }) 24 | export class CoreModule { 25 | static forRoot(): ModuleWithProviders { 26 | return { 27 | ngModule: CoreModule 28 | }; 29 | } 30 | 31 | constructor( 32 | @Optional() 33 | @SkipSelf() 34 | parentModule?: CoreModule 35 | ) { 36 | if (parentModule) { 37 | throw new Error( 38 | 'CoreModule is already loaded. Import it in the AppModule only' 39 | ); 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/app/core/db/root.db.service.ts: -------------------------------------------------------------------------------- 1 | import { generateUsers } from '@state/user/user.model'; 2 | import { InMemoryDbService } from 'angular-in-memory-web-api'; 3 | 4 | export class RootDbService implements InMemoryDbService { 5 | createDb() { 6 | return { users: generateUsers(10) }; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/app/core/services/user.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { HttpClient } from '@angular/common/http'; 2 | import { TestBed } from '@angular/core/testing'; 3 | import { cold } from 'jasmine-marbles'; 4 | import { generateUser, generateUsers } from './../../state/user/user.model'; 5 | import { UserService } from './user.service'; 6 | 7 | describe('UserService', () => { 8 | let service: UserService; 9 | let http: HttpClient; 10 | 11 | beforeEach(() => { 12 | TestBed.configureTestingModule({ 13 | providers: [ 14 | { provide: HttpClient, useValue: { get: jest.fn() } }, 15 | UserService 16 | ] 17 | }); 18 | 19 | http = TestBed.get(HttpClient); 20 | service = TestBed.get(UserService); 21 | 22 | // Mock implementation of console.error to 23 | // return undefined to stop printing out to console log during test 24 | jest.spyOn(console, 'error').mockImplementation(() => undefined); 25 | }); 26 | 27 | it('should create an instance successfully', () => { 28 | expect(service).toBeDefined(); 29 | }); 30 | 31 | it('should add a user', () => { 32 | const user = generateUser(); 33 | const expected = cold('-a|', { a: user }); 34 | http.post = jest.fn(() => expected); 35 | 36 | expect(service.addUser(user)).toBeObservable(expected); 37 | expect(http.post).toHaveBeenCalledWith(UserService.BASE_URL, user); 38 | }); 39 | 40 | it('should retreive a user', () => { 41 | const user = generateUser(); 42 | const expected = cold('-a|', { a: user }); 43 | http.get = jest.fn(() => expected); 44 | 45 | expect(service.getUser(user.id)).toBeObservable(expected); 46 | expect(http.get).toHaveBeenCalledWith(`${UserService.BASE_URL}/${user.id}`); 47 | }); 48 | 49 | it('should retrieve all users', () => { 50 | const expected = cold('-b|', { b: generateUsers() }); 51 | http.get = jest.fn(() => expected); 52 | 53 | expect(service.getUsers()).toBeObservable(expected); 54 | expect(http.get).toHaveBeenCalled(); 55 | }); 56 | 57 | it('should update a user', () => { 58 | const user = generateUser(); 59 | const expected = cold('-a|', { a: user }); 60 | http.put = jest.fn(() => expected); 61 | 62 | expect(service.updateUser(user)).toBeObservable(expected); 63 | expect(http.put).toHaveBeenCalledWith( 64 | `${UserService.BASE_URL}/${user.id}`, 65 | user 66 | ); 67 | }); 68 | }); 69 | -------------------------------------------------------------------------------- /src/app/core/services/user.service.ts: -------------------------------------------------------------------------------- 1 | import { HttpClient } from '@angular/common/http'; 2 | import { Injectable } from '@angular/core'; 3 | import { User } from '@state/user/user.model'; 4 | import { Observable } from 'rxjs'; 5 | 6 | @Injectable({ 7 | providedIn: 'root' 8 | }) 9 | export class UserService { 10 | static BASE_URL = 'api/users'; 11 | 12 | constructor(private httpClient: HttpClient) {} 13 | 14 | addUser(user: Partial): Observable { 15 | return this.httpClient.post(UserService.BASE_URL, user); 16 | } 17 | 18 | getUser(id: number): Observable { 19 | return this.httpClient.get(`${UserService.BASE_URL}/${id}`); 20 | } 21 | 22 | getUsers(): Observable> { 23 | return this.httpClient.get>(UserService.BASE_URL); 24 | } 25 | 26 | updateUser(user: Partial): Observable { 27 | return this.httpClient.put( 28 | `${UserService.BASE_URL}/${user.id}`, 29 | user 30 | ); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/app/state/app.effects.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | import { provideMockActions } from '@ngrx/effects/testing'; 3 | import { Observable } from 'rxjs'; 4 | import { AppEffects } from './app.effects'; 5 | 6 | describe('AppService', () => { 7 | const actions$ = new Observable(); 8 | let effects: AppEffects; 9 | 10 | beforeEach(() => { 11 | TestBed.configureTestingModule({ 12 | providers: [AppEffects, provideMockActions(() => actions$)] 13 | }); 14 | 15 | effects = TestBed.get(AppEffects); 16 | }); 17 | 18 | it('should be created', () => { 19 | expect(effects).toBeTruthy(); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /src/app/state/app.effects.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Actions } from '@ngrx/effects'; 3 | 4 | @Injectable() 5 | export class AppEffects { 6 | constructor(private actions$: Actions) {} 7 | } 8 | -------------------------------------------------------------------------------- /src/app/state/index.ts: -------------------------------------------------------------------------------- 1 | import * as fromRouter from '@ngrx/router-store'; 2 | import { ActionReducerMap, MetaReducer } from '@ngrx/store'; 3 | import { environment } from '../../environments/environment'; 4 | import { RouterStateUrl } from './shared/utils'; 5 | import * as fromUser from './user/user.reducer'; 6 | 7 | export interface AppState { 8 | router: fromRouter.RouterReducerState; 9 | user: fromUser.State; 10 | } 11 | 12 | export type State = AppState; 13 | 14 | export const reducers: ActionReducerMap = { 15 | router: fromRouter.routerReducer, 16 | user: fromUser.reducer 17 | }; 18 | 19 | export const metaReducers: MetaReducer[] = !environment.production 20 | ? [] 21 | : []; 22 | -------------------------------------------------------------------------------- /src/app/state/shared/utils.ts: -------------------------------------------------------------------------------- 1 | import { Params, RouterStateSnapshot } from '@angular/router'; 2 | import { RouterStateSerializer } from '@ngrx/router-store'; 3 | 4 | /** 5 | * The RouterStateSerializer takes the current RouterStateSnapshot 6 | * and returns any pertinent information needed. The snapshot contains 7 | * all information about the state of the router at the given point in time. 8 | * The entire snapshot is complex and not always needed. In this case, you only 9 | * need the URL and query parameters from the snapshot in the store. Other items could be 10 | * returned such as route parameters and static route data. 11 | */ 12 | 13 | export interface RouterStateUrl { 14 | url: string; 15 | params: Params; 16 | queryParams: Params; 17 | } 18 | 19 | export class CustomRouterStateSerializer 20 | implements RouterStateSerializer { 21 | serialize(routerState: RouterStateSnapshot): RouterStateUrl { 22 | let route = routerState.root; 23 | 24 | while (route.firstChild) { 25 | route = route.firstChild; 26 | } 27 | 28 | const { 29 | url, 30 | root: { queryParams } 31 | } = routerState; 32 | const { params } = route; 33 | 34 | // Only return an object including the URL, params and query params 35 | // instead of the entire snapshot 36 | return { url, params, queryParams }; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/app/state/state.module.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | import { Store } from '@ngrx/store'; 3 | import { StateModule } from '@state/state.module'; 4 | 5 | describe(`StateModule`, () => { 6 | beforeEach(() => { 7 | TestBed.configureTestingModule({ 8 | imports: [StateModule.forRoot()] 9 | }); 10 | }); 11 | 12 | it(`should provide 'Store' service`, () => { 13 | expect(() => TestBed.get(Store)).toBeTruthy(); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /src/app/state/state.module.ts: -------------------------------------------------------------------------------- 1 | import { CommonModule } from '@angular/common'; 2 | import { 3 | ModuleWithProviders, 4 | NgModule, 5 | Optional, 6 | SkipSelf 7 | } from '@angular/core'; 8 | import { EffectsModule } from '@ngrx/effects'; 9 | import { StoreRouterConnectingModule } from '@ngrx/router-store'; 10 | import { StoreModule } from '@ngrx/store'; 11 | import { StoreDevtoolsModule } from '@ngrx/store-devtools'; 12 | import { environment } from './../../environments/environment'; 13 | import { metaReducers, reducers } from './index'; 14 | import { UserEffects } from './user/user.effects'; 15 | 16 | @NgModule({ 17 | imports: [ 18 | CommonModule, 19 | StoreRouterConnectingModule, 20 | StoreModule.forRoot(reducers, { metaReducers: metaReducers }), 21 | EffectsModule.forRoot([UserEffects]), 22 | StoreDevtoolsModule.instrument({ 23 | name: 'NgRx Testing Store DevTools', 24 | logOnly: environment.production 25 | }) 26 | ], 27 | declarations: [] 28 | }) 29 | export class StateModule { 30 | static forRoot(): ModuleWithProviders { 31 | return { 32 | ngModule: StateModule 33 | }; 34 | } 35 | 36 | constructor( 37 | @Optional() 38 | @SkipSelf() 39 | parentModule?: StateModule 40 | ) { 41 | if (parentModule) { 42 | throw new Error( 43 | 'StateModule is already loaded. Import it in the AppModule only' 44 | ); 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/app/state/user/index.spec.ts: -------------------------------------------------------------------------------- 1 | import { generateUsers } from '@state/user/user.model'; 2 | import { selectSelectedUser } from './index'; 3 | 4 | describe('User selectors', () => { 5 | const users = generateUsers(2); 6 | const entities = { 7 | [users[0].id]: users[0], 8 | [users[1].id]: users[1] 9 | }; 10 | const selectedUser = users[0]; 11 | 12 | it('should return null when entities is falsy', () => { 13 | expect(selectSelectedUser.projector(null, selectedUser.id)).toBe(null); 14 | }); 15 | 16 | it('should get the retrieve the selected user', () => { 17 | expect(selectSelectedUser.projector(entities, selectedUser.id)).toBe( 18 | selectedUser 19 | ); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /src/app/state/user/index.ts: -------------------------------------------------------------------------------- 1 | import { createFeatureSelector, createSelector } from '@ngrx/store'; 2 | import { State } from '@state/user/user.reducer'; 3 | import * as fromUser from './user.reducer'; 4 | 5 | export const selectUserState = createFeatureSelector('user'); 6 | 7 | export const selectUserIds = createSelector( 8 | selectUserState, 9 | fromUser.selectUserIds 10 | ); 11 | export const selectUserEntities = createSelector( 12 | selectUserState, 13 | fromUser.selectUserEntities 14 | ); 15 | export const selectAllUsers = createSelector( 16 | selectUserState, 17 | fromUser.selectAllUsers 18 | ); 19 | export const selectUserTotal = createSelector( 20 | selectUserState, 21 | fromUser.selectUserTotal 22 | ); 23 | export const selectSelectedUserId = createSelector( 24 | selectUserState, 25 | fromUser.getSelectedUserId 26 | ); 27 | export const selectSelectedUser = createSelector( 28 | selectUserEntities, 29 | selectSelectedUserId, 30 | (entities, selectedUserId) => entities && entities[selectedUserId] 31 | ); 32 | -------------------------------------------------------------------------------- /src/app/state/user/user.actions.ts: -------------------------------------------------------------------------------- 1 | import { Update } from '@ngrx/entity'; 2 | import { Action } from '@ngrx/store'; 3 | import { User } from './user.model'; 4 | 5 | export enum UserActionTypes { 6 | LoadUser = '[User] Load User', 7 | LoadUserSuccess = '[User] Load User Success', 8 | LoadUserFail = '[User] Load User Fail', 9 | LoadUsers = '[User] Load Users', 10 | LoadUsersSuccess = '[User] Load Users Success', 11 | LoadUsersFail = '[User] Load Users Fail', 12 | AddUser = '[User] Add User', 13 | AddUserSuccess = '[User] Add User Success', 14 | AddUserFail = '[User] Add User Fail', 15 | AddUsers = '[User] Add Users', 16 | AddUsersSuccess = '[User] Add Users Success', 17 | AddUsersFail = '[User] Add Users Fail', 18 | UpdateUser = '[User] Update User', 19 | UpdateUserSuccess = '[User] Update User Success', 20 | UpdateUserFail = '[User] Update User Fail', 21 | UpdateUsers = '[User] Update Users', 22 | UpdateUsersSuccess = '[User] Update Users Success', 23 | UpdateUsersFail = '[User] Update Users Fail', 24 | DeleteUser = '[User] Delete User', 25 | DeleteUserSuccess = '[User] Delete User Success', 26 | DeleteUserFail = '[User] Delete User Fail', 27 | DeleteUsers = '[User] Delete Users', 28 | ClearUsers = '[User] Clear Users', 29 | SelectUser = '[User] Select User' 30 | } 31 | 32 | export class LoadUser implements Action { 33 | readonly type = UserActionTypes.LoadUser; 34 | 35 | constructor(public payload: { id: number }) {} 36 | } 37 | 38 | export class LoadUserSuccess implements Action { 39 | readonly type = UserActionTypes.LoadUserSuccess; 40 | 41 | constructor(public payload: { user: User }) {} 42 | } 43 | 44 | export class LoadUserFail implements Action { 45 | readonly type = UserActionTypes.LoadUserFail; 46 | 47 | constructor(public payload: { error: Error }) {} 48 | } 49 | 50 | export class LoadUsers implements Action { 51 | readonly type = UserActionTypes.LoadUsers; 52 | } 53 | 54 | export class LoadUsersSuccess implements Action { 55 | readonly type = UserActionTypes.LoadUsersSuccess; 56 | 57 | constructor(public payload: { users: User[] }) {} 58 | } 59 | 60 | export class LoadUsersFail implements Action { 61 | readonly type = UserActionTypes.LoadUsersFail; 62 | 63 | constructor(public payload: { error: Error }) {} 64 | } 65 | 66 | export class AddUser implements Action { 67 | readonly type = UserActionTypes.AddUser; 68 | 69 | constructor(public payload: { user: Partial }) {} 70 | } 71 | 72 | export class AddUserSuccess implements Action { 73 | readonly type = UserActionTypes.AddUserSuccess; 74 | 75 | constructor(public payload: { user: User }) {} 76 | } 77 | 78 | export class AddUserFail implements Action { 79 | readonly type = UserActionTypes.AddUserFail; 80 | 81 | constructor(public payload: { error: Error }) {} 82 | } 83 | 84 | export class AddUsers implements Action { 85 | readonly type = UserActionTypes.AddUsers; 86 | 87 | constructor(public payload: { users: Partial[] }) {} 88 | } 89 | 90 | export class AddUsersSuccess implements Action { 91 | readonly type = UserActionTypes.AddUsersSuccess; 92 | 93 | constructor(public payload: { users: User[] }) {} 94 | } 95 | 96 | export class AddUsersFail implements Action { 97 | readonly type = UserActionTypes.AddUsersFail; 98 | 99 | constructor(public payload: { error: Error }) {} 100 | } 101 | 102 | export class UpdateUser implements Action { 103 | readonly type = UserActionTypes.UpdateUser; 104 | 105 | constructor(public payload: { user: Partial }) {} 106 | } 107 | 108 | export class UpdateUserSuccess implements Action { 109 | readonly type = UserActionTypes.UpdateUserSuccess; 110 | 111 | constructor(public payload: { update: Update }) {} 112 | } 113 | 114 | export class UpdateUserFail implements Action { 115 | readonly type = UserActionTypes.UpdateUserFail; 116 | 117 | constructor(public payload: { error: Error }) {} 118 | } 119 | 120 | export class UpdateUsers implements Action { 121 | readonly type = UserActionTypes.UpdateUsers; 122 | 123 | constructor(public payload: { users: Partial[] }) {} 124 | } 125 | 126 | export class UpdateUsersSuccess implements Action { 127 | readonly type = UserActionTypes.UpdateUsersSuccess; 128 | 129 | constructor(public payload: { update: Update[] }) {} 130 | } 131 | 132 | export class UpdateUsersFail implements Action { 133 | readonly type = UserActionTypes.UpdateUsersFail; 134 | 135 | constructor(public payload: { error: Error }) {} 136 | } 137 | 138 | export class DeleteUser implements Action { 139 | readonly type = UserActionTypes.DeleteUser; 140 | 141 | constructor(public payload: { id: string }) {} 142 | } 143 | 144 | export class DeleteUserSuccess implements Action { 145 | readonly type = UserActionTypes.DeleteUserSuccess; 146 | 147 | constructor(public payload: { id: string }) {} 148 | } 149 | 150 | export class DeleteUserFail implements Action { 151 | readonly type = UserActionTypes.DeleteUserFail; 152 | 153 | constructor(public payload: { error: Error }) {} 154 | } 155 | 156 | export class DeleteUsers implements Action { 157 | readonly type = UserActionTypes.DeleteUsers; 158 | 159 | constructor(public payload: { ids: string[] }) {} 160 | } 161 | 162 | export class ClearUsers implements Action { 163 | readonly type = UserActionTypes.ClearUsers; 164 | } 165 | 166 | export class SelectUser implements Action { 167 | readonly type = UserActionTypes.SelectUser; 168 | 169 | constructor(public payload: { id: number }) {} 170 | } 171 | 172 | export type UserActions = 173 | | LoadUser 174 | | LoadUserSuccess 175 | | LoadUserFail 176 | | LoadUsers 177 | | LoadUsersSuccess 178 | | LoadUsersFail 179 | | AddUser 180 | | AddUserSuccess 181 | | AddUserFail 182 | | AddUsers 183 | | AddUsersSuccess 184 | | AddUsersFail 185 | | UpdateUser 186 | | UpdateUserSuccess 187 | | UpdateUserFail 188 | | UpdateUsers 189 | | UpdateUsersSuccess 190 | | UpdateUsersFail 191 | | DeleteUser 192 | | DeleteUserSuccess 193 | | DeleteUserFail 194 | | DeleteUsers 195 | | ClearUsers 196 | | SelectUser; 197 | -------------------------------------------------------------------------------- /src/app/state/user/user.effects.spec.ts: -------------------------------------------------------------------------------- 1 | import { provideMockActions } from '@ngrx/effects/testing'; 2 | import { TestBed } from '@angular/core/testing'; 3 | import { UserService } from '@core/services/user.service'; 4 | import { cold, hot } from 'jasmine-marbles'; 5 | import { Observable, empty } from 'rxjs'; 6 | import { 7 | AddUser, 8 | AddUserFail, 9 | AddUserSuccess, 10 | LoadUser, 11 | LoadUserFail, 12 | LoadUserSuccess, 13 | LoadUsers, 14 | LoadUsersFail, 15 | LoadUsersSuccess, 16 | UpdateUser, 17 | UpdateUserFail, 18 | UpdateUserSuccess 19 | } from './user.actions'; 20 | import { UserEffects } from './user.effects'; 21 | import { generateUser, generateUsers } from './user.model'; 22 | 23 | describe('UserEffects', () => { 24 | let actions: Observable; 25 | let effects: UserEffects; 26 | let userService: UserService; 27 | 28 | beforeEach(() => { 29 | TestBed.configureTestingModule({ 30 | providers: [ 31 | UserEffects, 32 | provideMockActions(() => actions) 33 | { 34 | provide: UserService, 35 | useValue: { 36 | addUser: jest.fn(), 37 | getUser: jest.fn(), 38 | getUsers: jest.fn(), 39 | updateUser: jest.fn() 40 | } 41 | } 42 | ] 43 | }); 44 | 45 | effects = TestBed.get(UserEffects); 46 | userService = TestBed.get(UserService); 47 | }); 48 | 49 | it('should be created', () => { 50 | expect(effects).toBeTruthy(); 51 | }); 52 | 53 | describe('addUser', () => { 54 | it('should return an AddUserSuccess action, with the user, on success', () => { 55 | const user = generateUser(); 56 | const action = new AddUser({ user }); 57 | const outcome = new AddUserSuccess({ user }); 58 | 59 | actions = hot('-a', { a: action }); 60 | const response = cold('-a|', { a: user }); 61 | const expected = cold('--b', { b: outcome }); 62 | userService.addUser = jest.fn(() => response); 63 | 64 | expect(effects.addUser).toBeObservable(expected); 65 | }); 66 | 67 | it('should return an AddUserFail action, with an error, on failure', () => { 68 | const user = generateUser(); 69 | const action = new AddUser({ user }); 70 | const error = new Error(); 71 | const outcome = new AddUserFail({ error }); 72 | 73 | actions = hot('-a', { a: action }); 74 | const response = cold('-#|', {}, error); 75 | const expected = cold('--(b|)', { b: outcome }); 76 | userService.addUser = jest.fn(() => response); 77 | 78 | expect(effects.addUser).toBeObservable(expected); 79 | }); 80 | }); 81 | 82 | describe('loadUsers', () => { 83 | it('should return a LoadUsersSuccess action, with the users, on success', () => { 84 | const users = generateUsers(); 85 | const action = new LoadUsers(); 86 | const outcome = new LoadUsersSuccess({ users: users }); 87 | 88 | actions = hot('-a', { a: action }); 89 | const response = cold('-a|', { a: users }); 90 | const expected = cold('--b', { b: outcome }); 91 | userService.getUsers = jest.fn(() => response); 92 | 93 | expect(effects.loadUsers).toBeObservable(expected); 94 | }); 95 | 96 | it('should return a LoadUsersFail action, with an error, on failure', () => { 97 | const action = new LoadUsers(); 98 | const error = new Error(); 99 | const outcome = new LoadUsersFail({ error: error }); 100 | 101 | actions = hot('-a', { a: action }); 102 | const response = cold('-#|', {}, error); 103 | const expected = cold('--(b|)', { b: outcome }); 104 | userService.getUsers = jest.fn(() => response); 105 | 106 | expect(effects.loadUsers).toBeObservable(expected); 107 | }); 108 | }); 109 | 110 | describe('loadUser', () => { 111 | it('should return a LoadUserSuccess action, with the user, on success', () => { 112 | const user = generateUser(); 113 | const action = new LoadUser({ id: user.id }); 114 | const outcome = new LoadUserSuccess({ user }); 115 | 116 | actions = hot('-a', { a: action }); 117 | const response = cold('-a|', { a: user }); 118 | const expected = cold('--b', { b: outcome }); 119 | userService.getUser = jest.fn(() => response); 120 | 121 | expect(effects.loadUser).toBeObservable(expected); 122 | }); 123 | 124 | it('should return a LoadUserFail action, with an error, on failure', () => { 125 | const user = generateUser(); 126 | const action = new LoadUser({ id: user.id }); 127 | const error = new Error(); 128 | const outcome = new LoadUserFail({ error }); 129 | 130 | actions = hot('-a', { a: action }); 131 | const response = cold('-#|', {}, error); 132 | const expected = cold('--(b|)', { b: outcome }); 133 | userService.getUser = jest.fn(() => response); 134 | 135 | expect(effects.loadUser).toBeObservable(expected); 136 | }); 137 | }); 138 | 139 | describe('updateUser', () => { 140 | it('should return an UpdateUserSuccess action, with the user, on success', () => { 141 | const user = generateUser(); 142 | const action = new UpdateUser({ user }); 143 | const outcome = new UpdateUserSuccess({ 144 | update: { 145 | id: user.id, 146 | changes: user 147 | } 148 | }); 149 | 150 | actions = hot('-a', { a: action }); 151 | const response = cold('-a|', { a: user }); 152 | const expected = cold('--b', { b: outcome }); 153 | userService.updateUser = jest.fn(() => response); 154 | 155 | expect(effects.updateUser).toBeObservable(expected); 156 | }); 157 | 158 | it('should return an UpdateUserFail action, with an error, on failure', () => { 159 | const user = generateUser(); 160 | const action = new UpdateUser({ user }); 161 | const error = new Error(); 162 | const outcome = new UpdateUserFail({ error }); 163 | 164 | actions = hot('-a', { a: action }); 165 | const response = cold('-#|', {}, error); 166 | const expected = cold('--(b|)', { b: outcome }); 167 | userService.updateUser = jest.fn(() => response); 168 | 169 | expect(effects.updateUser).toBeObservable(expected); 170 | }); 171 | }); 172 | }); 173 | -------------------------------------------------------------------------------- /src/app/state/user/user.effects.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { UserService } from '@core/services/user.service'; 3 | import { Actions, Effect } from '@ngrx/effects'; 4 | import { Action } from '@ngrx/store'; 5 | import { Observable, of } from 'rxjs'; 6 | import { catchError, exhaustMap, map } from 'rxjs/operators'; 7 | import { 8 | AddUser, 9 | AddUserFail, 10 | AddUserSuccess, 11 | LoadUser, 12 | LoadUserFail, 13 | LoadUserSuccess, 14 | LoadUsers, 15 | LoadUsersFail, 16 | LoadUsersSuccess, 17 | UpdateUser, 18 | UpdateUserFail, 19 | UpdateUserSuccess, 20 | UserActionTypes 21 | } from './user.actions'; 22 | 23 | @Injectable() 24 | export class UserEffects { 25 | @Effect() 26 | addUser: Observable = this.actions$ 27 | .ofType(UserActionTypes.AddUser) 28 | .pipe( 29 | map(action => action.payload), 30 | exhaustMap(payload => this.userService.addUser(payload.user)), 31 | map(user => new AddUserSuccess({ user })), 32 | catchError(error => of(new AddUserFail({ error }))) 33 | ); 34 | 35 | @Effect() 36 | loadUser: Observable = this.actions$ 37 | .ofType(UserActionTypes.LoadUser) 38 | .pipe( 39 | map(action => action.payload), 40 | exhaustMap(payload => this.userService.getUser(payload.id)), 41 | map(user => new LoadUserSuccess({ user })), 42 | catchError(error => of(new LoadUserFail({ error }))) 43 | ); 44 | 45 | @Effect() 46 | loadUsers: Observable = this.actions$ 47 | .ofType(UserActionTypes.LoadUsers) 48 | .pipe( 49 | exhaustMap(() => this.userService.getUsers()), 50 | map(users => new LoadUsersSuccess({ users })), 51 | catchError(error => of(new LoadUsersFail({ error }))) 52 | ); 53 | 54 | @Effect() 55 | updateUser: Observable = this.actions$ 56 | .ofType(UserActionTypes.UpdateUser) 57 | .pipe( 58 | map(action => action.payload), 59 | exhaustMap(payload => this.userService.updateUser(payload.user)), 60 | map( 61 | user => 62 | new UpdateUserSuccess({ 63 | update: { 64 | id: user.id, 65 | changes: user 66 | } 67 | }) 68 | ), 69 | catchError(error => of(new UpdateUserFail({ error }))) 70 | ); 71 | 72 | constructor(private actions$: Actions, private userService: UserService) {} 73 | } 74 | -------------------------------------------------------------------------------- /src/app/state/user/user.model.ts: -------------------------------------------------------------------------------- 1 | import * as faker from 'faker/locale/en_US'; 2 | 3 | export interface User { 4 | id: number; 5 | firstName: string; 6 | lastName: string; 7 | } 8 | 9 | export const generateUser = (): User => { 10 | return { 11 | id: faker.random.number(), 12 | firstName: faker.name.firstName(), 13 | lastName: faker.name.lastName() 14 | }; 15 | }; 16 | 17 | export const generateUsers = ( 18 | count = faker.random.number({ min: 1, max: 20 }) 19 | ): User[] => { 20 | return Array.apply(null, Array(count)).map(() => generateUser()); 21 | }; 22 | -------------------------------------------------------------------------------- /src/app/state/user/user.reducer.spec.ts: -------------------------------------------------------------------------------- 1 | import { User } from '@state/user/user.model'; 2 | import { 3 | AddUser, 4 | AddUserFail, 5 | AddUserSuccess, 6 | AddUsersSuccess, 7 | LoadUser, 8 | LoadUserFail, 9 | LoadUserSuccess, 10 | LoadUsers, 11 | LoadUsersFail, 12 | LoadUsersSuccess, 13 | SelectUser, 14 | UpdateUser, 15 | UpdateUserFail, 16 | UpdateUserSuccess, 17 | UpdateUsers, 18 | UpdateUsersSuccess 19 | } from './user.actions'; 20 | import { initialState, reducer } from './user.reducer'; 21 | 22 | describe('User Reducer', () => { 23 | const anakin: User = { 24 | id: 1, 25 | firstName: 'Anakin', 26 | lastName: 'Skywalker' 27 | }; 28 | 29 | describe('undefined action', () => { 30 | it('should return the default state', () => { 31 | const action = { type: 'NOOP' } as any; 32 | const result = reducer(undefined, action); 33 | 34 | expect(result).toBe(initialState); 35 | }); 36 | }); 37 | 38 | describe('[User] Add User', () => { 39 | it('should toggle loading state', () => { 40 | const action = new AddUser({ user: anakin }); 41 | const result = reducer(initialState, action); 42 | 43 | expect(result).toEqual({ 44 | ...initialState, 45 | error: undefined, 46 | loading: true 47 | }); 48 | }); 49 | }); 50 | 51 | describe('[User] Add User Success', () => { 52 | it('should add a user to state', () => { 53 | const action = new AddUserSuccess({ user: anakin }); 54 | const result = reducer(initialState, action); 55 | 56 | expect(result).toEqual({ 57 | ...initialState, 58 | entities: { 59 | [anakin.id]: anakin 60 | }, 61 | ids: [anakin.id], 62 | loading: false 63 | }); 64 | }); 65 | }); 66 | 67 | describe('[User] Add User Fail', () => { 68 | it('should update error in state', () => { 69 | const error = new Error(); 70 | const action = new AddUserFail({ error }); 71 | const result = reducer(initialState, action); 72 | 73 | expect(result).toEqual({ 74 | ...initialState, 75 | error, 76 | loading: false 77 | }); 78 | }); 79 | }); 80 | 81 | describe('[User] Load User', () => { 82 | it('should toggle loading state', () => { 83 | const action = new LoadUser({ id: anakin.id }); 84 | const result = reducer(initialState, action); 85 | 86 | expect(result).toEqual({ 87 | ...initialState, 88 | error: undefined, 89 | loading: true 90 | }); 91 | }); 92 | }); 93 | 94 | describe('[User] Load User Success', () => { 95 | it('should load a user to state', () => { 96 | const action = new LoadUserSuccess({ user: anakin }); 97 | const result = reducer(initialState, action); 98 | 99 | expect(result).toEqual({ 100 | ...initialState, 101 | entities: { 102 | [anakin.id]: anakin 103 | }, 104 | ids: [anakin.id], 105 | loading: false 106 | }); 107 | }); 108 | }); 109 | 110 | describe('[User] Load User Fail', () => { 111 | it('should update error in state', () => { 112 | const error = new Error(); 113 | const action = new LoadUserFail({ error }); 114 | const result = reducer(initialState, action); 115 | 116 | expect(result).toEqual({ 117 | ...initialState, 118 | error, 119 | loading: false 120 | }); 121 | }); 122 | }); 123 | 124 | describe('[User] Load Users', () => { 125 | it('should toggle loading state', () => { 126 | const action = new LoadUsers(); 127 | const result = reducer(initialState, action); 128 | 129 | expect(result).toEqual({ 130 | ...initialState, 131 | error: undefined, 132 | loading: true 133 | }); 134 | }); 135 | }); 136 | 137 | describe('[User] Load Users Success', () => { 138 | it('should add all users to state', () => { 139 | const users = [anakin]; 140 | const action = new LoadUsersSuccess({ users }); 141 | const result = reducer(initialState, action); 142 | 143 | expect(result).toEqual({ 144 | ...initialState, 145 | entities: users.reduce( 146 | (entityMap, user) => ({ 147 | ...entityMap, 148 | [user.id]: user 149 | }), 150 | {} 151 | ), 152 | ids: users.map(user => user.id), 153 | loading: false 154 | }); 155 | }); 156 | }); 157 | 158 | describe('[User] Load Users Fail', () => { 159 | it('should update error in state', () => { 160 | const users = [anakin]; 161 | const error = new Error(); 162 | const action = new LoadUsersFail({ error }); 163 | const result = reducer(initialState, action); 164 | 165 | expect(result).toEqual({ 166 | ...initialState, 167 | error, 168 | loading: false 169 | }); 170 | }); 171 | }); 172 | 173 | describe('[User] Update User', () => { 174 | it('should toggle loading state', () => { 175 | const action = new UpdateUser({ 176 | user: { ...anakin, firstName: 'Darth', lastName: 'Vader' } 177 | }); 178 | const result = reducer(initialState, action); 179 | 180 | expect(result).toEqual({ 181 | ...initialState, 182 | error: undefined, 183 | loading: true 184 | }); 185 | }); 186 | }); 187 | 188 | describe('[User] Update User Success', () => { 189 | it('should update user in state', () => { 190 | const updatedUser: User = { 191 | ...anakin, 192 | firstName: 'Darth', 193 | lastName: 'Vader' 194 | }; 195 | const action = new UpdateUserSuccess({ 196 | update: { 197 | id: anakin.id, 198 | changes: updatedUser 199 | } 200 | }); 201 | 202 | const state = reducer(initialState, new AddUserSuccess({ user: anakin })); 203 | expect(state).toEqual({ 204 | ...initialState, 205 | entities: { 206 | [anakin.id]: anakin 207 | }, 208 | ids: [anakin.id], 209 | loading: false 210 | }); 211 | 212 | const result = reducer(state, action); 213 | expect(result).toEqual({ 214 | ...state, 215 | entities: { 216 | ...state.entities, 217 | [anakin.id]: updatedUser 218 | }, 219 | ids: [...state.ids], 220 | loading: false 221 | }); 222 | }); 223 | }); 224 | 225 | describe('[User] Update User Fail', () => { 226 | it('should update error in state', () => { 227 | const users = [anakin]; 228 | const error = new Error(); 229 | const action = new UpdateUserFail({ error }); 230 | const result = reducer(initialState, action); 231 | 232 | expect(result).toEqual({ 233 | ...initialState, 234 | error, 235 | loading: false 236 | }); 237 | }); 238 | }); 239 | 240 | describe('[User] Update Users', () => { 241 | it('should toggle loading state', () => { 242 | const action = new UpdateUsers({ 243 | users: [{ ...anakin, firstName: 'Darth', lastName: 'Vader' }] 244 | }); 245 | const result = reducer(initialState, action); 246 | 247 | expect(result).toEqual({ 248 | ...initialState, 249 | error: undefined, 250 | loading: true 251 | }); 252 | }); 253 | }); 254 | 255 | describe('[User] Update Users Success', () => { 256 | it('should add all users to state', () => { 257 | const senator = { 258 | id: 2, 259 | firstName: 'Sheev', 260 | lastName: 'Palpaatine' 261 | }; 262 | const vader = { 263 | ...anakin, 264 | firstName: 'Darth', 265 | lastName: 'Vader' 266 | }; 267 | const sidious = { 268 | ...senator, 269 | firstName: 'Darth', 270 | lastName: 'Sidious' 271 | }; 272 | const originalUsers = [anakin, senator]; 273 | const updatedUsers = [vader, sidious]; 274 | 275 | const state = reducer( 276 | initialState, 277 | new AddUsersSuccess({ 278 | users: originalUsers 279 | }) 280 | ); 281 | 282 | const action = new UpdateUsersSuccess({ 283 | update: [ 284 | { 285 | id: anakin.id, 286 | changes: vader 287 | }, 288 | { 289 | id: senator.id, 290 | changes: sidious 291 | } 292 | ] 293 | }); 294 | const result = reducer(state, action); 295 | 296 | expect(result).toEqual({ 297 | ...state, 298 | entities: updatedUsers.reduce( 299 | (entityMap, user) => ({ 300 | ...entityMap, 301 | [user.id]: user 302 | }), 303 | {} 304 | ), 305 | ids: updatedUsers.map(user => user.id), 306 | loading: false 307 | }); 308 | }); 309 | }); 310 | 311 | describe('[User] Select User', () => { 312 | it('should set the selectedUserId property in state', () => { 313 | const action = new SelectUser({ id: anakin.id }); 314 | const result = reducer(initialState, action); 315 | 316 | expect(result).toEqual({ 317 | ...initialState, 318 | selectedUserId: anakin.id 319 | }); 320 | }); 321 | }); 322 | }); 323 | -------------------------------------------------------------------------------- /src/app/state/user/user.reducer.ts: -------------------------------------------------------------------------------- 1 | import { EntityAdapter, EntityState, createEntityAdapter } from '@ngrx/entity'; 2 | import { UserActionTypes, UserActions } from './user.actions'; 3 | import { User } from './user.model'; 4 | 5 | export interface State extends EntityState { 6 | error?: Error; 7 | loading: boolean; 8 | selectedUserId: number; 9 | } 10 | 11 | export const adapter: EntityAdapter = createEntityAdapter(); 12 | 13 | export const initialState: State = adapter.getInitialState({ 14 | loading: false, 15 | selectedUserId: null 16 | }); 17 | 18 | export function reducer(state = initialState, action: UserActions): State { 19 | switch (action.type) { 20 | case UserActionTypes.AddUser: 21 | case UserActionTypes.AddUsers: 22 | case UserActionTypes.LoadUser: 23 | case UserActionTypes.LoadUsers: 24 | case UserActionTypes.UpdateUser: 25 | case UserActionTypes.UpdateUsers: { 26 | return { ...state, loading: true, error: undefined }; 27 | } 28 | 29 | case UserActionTypes.AddUserSuccess: 30 | case UserActionTypes.LoadUserSuccess: { 31 | return adapter.addOne(action.payload.user, { ...state, loading: false }); 32 | } 33 | 34 | case UserActionTypes.AddUserFail: 35 | case UserActionTypes.AddUsersFail: 36 | case UserActionTypes.LoadUserFail: 37 | case UserActionTypes.LoadUsersFail: 38 | case UserActionTypes.UpdateUserFail: 39 | case UserActionTypes.UpdateUsersFail: { 40 | return { ...state, error: action.payload.error, loading: false }; 41 | } 42 | 43 | case UserActionTypes.AddUsersSuccess: { 44 | return adapter.addMany(action.payload.users, { 45 | ...state, 46 | loading: false 47 | }); 48 | } 49 | 50 | case UserActionTypes.UpdateUserSuccess: { 51 | return adapter.updateOne(action.payload.update, { 52 | ...state, 53 | loading: false 54 | }); 55 | } 56 | 57 | case UserActionTypes.UpdateUsersSuccess: { 58 | return adapter.updateMany(action.payload.update, { 59 | ...state, 60 | loading: false 61 | }); 62 | } 63 | 64 | case UserActionTypes.DeleteUser: { 65 | return adapter.removeOne(action.payload.id, state); 66 | } 67 | 68 | case UserActionTypes.DeleteUsers: { 69 | return adapter.removeMany(action.payload.ids, state); 70 | } 71 | 72 | case UserActionTypes.LoadUsersSuccess: { 73 | return adapter.addAll(action.payload.users, { ...state, loading: false }); 74 | } 75 | 76 | case UserActionTypes.ClearUsers: { 77 | return adapter.removeAll(state); 78 | } 79 | 80 | case UserActionTypes.SelectUser: { 81 | return { ...state, selectedUserId: action.payload.id }; 82 | } 83 | 84 | default: { 85 | return state; 86 | } 87 | } 88 | } 89 | 90 | export const getSelectedUserId = (state: State) => state.selectedUserId; 91 | 92 | export const { 93 | selectIds: selectUserIds, 94 | selectEntities: selectUserEntities, 95 | selectAll: selectAllUsers, 96 | selectTotal: selectUserTotal 97 | } = adapter.getSelectors(); 98 | -------------------------------------------------------------------------------- /src/app/users/components/user-form/__snapshots__/user-form.component.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`UserFormComponent should match snapshot 1`] = ` 4 | 9 |
14 | 21 | 28 | 31 |
32 |
33 | `; 34 | -------------------------------------------------------------------------------- /src/app/users/components/user-form/user-form.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 5 |
-------------------------------------------------------------------------------- /src/app/users/components/user-form/user-form.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blove/ngrx-testing/5449c76871375ead6be7df7f080bcebba54c5c7c/src/app/users/components/user-form/user-form.component.scss -------------------------------------------------------------------------------- /src/app/users/components/user-form/user-form.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { SimpleChange } from '@angular/core'; 2 | import { ComponentFixture, TestBed, async } from '@angular/core/testing'; 3 | import { FormsModule, ReactiveFormsModule } from '@angular/forms'; 4 | import { By } from '@angular/platform-browser'; 5 | import { User, generateUser } from '@state/user/user.model'; 6 | import { UserFormComponent } from './user-form.component'; 7 | 8 | function newEvent(eventName: string, bubbles = false, cancelable = false) { 9 | const evt = document.createEvent('CustomEvent'); 10 | evt.initCustomEvent(eventName, bubbles, cancelable, null); 11 | return evt; 12 | } 13 | 14 | describe('UserFormComponent', () => { 15 | let component: UserFormComponent; 16 | let fixture: ComponentFixture; 17 | 18 | beforeEach(async(() => { 19 | TestBed.configureTestingModule({ 20 | declarations: [UserFormComponent], 21 | imports: [FormsModule, ReactiveFormsModule] 22 | }).compileComponents(); 23 | })); 24 | 25 | beforeEach(() => { 26 | fixture = TestBed.createComponent(UserFormComponent); 27 | component = fixture.componentInstance; 28 | }); 29 | 30 | it('should create', () => { 31 | expect(component).toBeTruthy(); 32 | }); 33 | 34 | it('should match snapshot', () => { 35 | fixture.detectChanges(); 36 | expect(fixture).toMatchSnapshot(); 37 | }); 38 | 39 | it('should patch values into the form', () => { 40 | const user = generateUser(); 41 | 42 | component.ngOnChanges({ 43 | user: new SimpleChange(null, user, true) 44 | }); 45 | 46 | expect(component.form.value).toEqual({ 47 | firstName: user.firstName, 48 | lastName: user.lastName 49 | }); 50 | }); 51 | 52 | it('should emit the userChange event when submitted', () => { 53 | const user = generateUser(); 54 | const firstName = 'Brian'; 55 | const firstNameDebugEl = fixture.debugElement.query( 56 | By.css('input[formControlName="firstName"]') 57 | ); 58 | const firstNameEl = firstNameDebugEl.nativeElement as HTMLInputElement; 59 | const buttonDebugEl = fixture.debugElement.query(By.css('button')); 60 | 61 | fixture.detectChanges(); 62 | 63 | let updatedUser: User; 64 | component.userChange.subscribe(u => (updatedUser = u)); 65 | 66 | component.user = user; 67 | component.ngOnChanges({ 68 | user: new SimpleChange(null, user, true) 69 | }); 70 | 71 | firstNameEl.value = firstName; 72 | firstNameEl.dispatchEvent(newEvent('input')); 73 | 74 | buttonDebugEl.triggerEventHandler('click', null); 75 | 76 | expect(updatedUser).toEqual({ 77 | ...user, 78 | firstName 79 | }); 80 | }); 81 | }); 82 | -------------------------------------------------------------------------------- /src/app/users/components/user-form/user-form.component.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Component, 3 | EventEmitter, 4 | Input, 5 | OnChanges, 6 | Output, 7 | SimpleChanges 8 | } from '@angular/core'; 9 | import { FormBuilder, FormGroup, Validators } from '@angular/forms'; 10 | import { User } from '@state/user/user.model'; 11 | 12 | @Component({ 13 | selector: 'app-user-form', 14 | templateUrl: './user-form.component.html', 15 | styleUrls: ['./user-form.component.scss'] 16 | }) 17 | export class UserFormComponent implements OnChanges { 18 | form: FormGroup; 19 | 20 | @Input() user: User; 21 | @Output() userChange = new EventEmitter(); 22 | 23 | constructor(private formBuilder: FormBuilder) { 24 | this.buildForm(); 25 | } 26 | 27 | ngOnChanges(simpleChanges: SimpleChanges) { 28 | if (simpleChanges.user && simpleChanges.user.currentValue) { 29 | this.form.patchValue(simpleChanges.user.currentValue); 30 | } 31 | } 32 | 33 | onSave() { 34 | // verify form is valid 35 | if (this.form.invalid) { 36 | return; 37 | } 38 | 39 | this.userChange.emit({ 40 | ...this.user, 41 | ...this.form.value 42 | }); 43 | } 44 | 45 | private buildForm() { 46 | this.form = this.formBuilder.group({ 47 | firstName: ['', Validators.required], 48 | lastName: ['', Validators.required] 49 | }); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/app/users/components/user-list/user-list.component.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/users/components/user-list/user-list.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blove/ngrx-testing/5449c76871375ead6be7df7f080bcebba54c5c7c/src/app/users/components/user-list/user-list.component.scss -------------------------------------------------------------------------------- /src/app/users/components/user-list/user-list.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed, async } from '@angular/core/testing'; 2 | import { By } from '@angular/platform-browser'; 3 | import { User, generateUsers } from '@state/user/user.model'; 4 | import { UserListComponent } from './user-list.component'; 5 | 6 | describe('UserListComponent', () => { 7 | let component: UserListComponent; 8 | let fixture: ComponentFixture; 9 | const users = generateUsers(); 10 | 11 | beforeEach(async(() => { 12 | TestBed.configureTestingModule({ 13 | declarations: [UserListComponent] 14 | }).compileComponents(); 15 | })); 16 | 17 | beforeEach(() => { 18 | fixture = TestBed.createComponent(UserListComponent); 19 | component = fixture.componentInstance; 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | 26 | it('should display an unordered list of heroes', () => { 27 | const ulDebugEl = fixture.debugElement.query(By.css('ul')); 28 | const ulEl = ulDebugEl.nativeElement as HTMLUListElement; 29 | component.users = users; 30 | fixture.detectChanges(); 31 | expect(ulEl.childElementCount).toBe(users.length); 32 | 33 | const firstLi = ulEl.querySelector('li:first-child'); 34 | expect(firstLi.textContent).toEqual( 35 | `${users[0].firstName} ${users[0].lastName}` 36 | ); 37 | }); 38 | 39 | it('should select a user when clicked', () => { 40 | const user = users[0]; 41 | 42 | component.users = users; 43 | fixture.detectChanges(); 44 | const anchorDebugEl = fixture.debugElement.query( 45 | By.css('ul > li:first-child > a') 46 | ); 47 | 48 | let selectedUser: User; 49 | component.selectUser.subscribe(u => (selectedUser = u)); 50 | 51 | anchorDebugEl.triggerEventHandler('click', user); 52 | expect(selectedUser).toEqual(user); 53 | }); 54 | }); 55 | -------------------------------------------------------------------------------- /src/app/users/components/user-list/user-list.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; 2 | import { User } from '@state/user/user.model'; 3 | 4 | @Component({ 5 | selector: 'app-user-list', 6 | templateUrl: './user-list.component.html', 7 | styleUrls: ['./user-list.component.scss'] 8 | }) 9 | export class UserListComponent implements OnInit { 10 | @Input() users: User[]; 11 | @Output() selectUser = new EventEmitter(); 12 | 13 | constructor() {} 14 | 15 | ngOnInit() {} 16 | } 17 | -------------------------------------------------------------------------------- /src/app/users/containers/add/add.component.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/users/containers/add/add.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blove/ngrx-testing/5449c76871375ead6be7df7f080bcebba54c5c7c/src/app/users/containers/add/add.component.scss -------------------------------------------------------------------------------- /src/app/users/containers/add/add.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed, async } from '@angular/core/testing'; 2 | import { FormsModule, ReactiveFormsModule } from '@angular/forms'; 3 | import { Store, StoreModule } from '@ngrx/store'; 4 | import * as fromRoot from '@state/index'; 5 | import { AddUser } from '@state/user/user.actions'; 6 | import { generateUser } from '@state/user/user.model'; 7 | import { UserFormComponent } from './../../components/user-form/user-form.component'; 8 | import { AddComponent } from './add.component'; 9 | 10 | describe('AddComponent', () => { 11 | let component: AddComponent; 12 | let fixture: ComponentFixture; 13 | let store: Store; 14 | 15 | beforeEach(async(() => { 16 | TestBed.configureTestingModule({ 17 | declarations: [AddComponent, UserFormComponent], 18 | imports: [ 19 | FormsModule, 20 | ReactiveFormsModule, 21 | StoreModule.forRoot(fromRoot.reducers) 22 | ] 23 | }).compileComponents(); 24 | })); 25 | 26 | beforeEach(() => { 27 | fixture = TestBed.createComponent(AddComponent); 28 | component = fixture.componentInstance; 29 | store = TestBed.get(Store); 30 | }); 31 | 32 | it('should create', () => { 33 | expect(component).toBeTruthy(); 34 | }); 35 | 36 | it('should dispatch the AddUser action when onUserChange is invoked', () => { 37 | const user = generateUser(); 38 | const action = new AddUser({ user }); 39 | const spy = jest.spyOn(store, 'dispatch'); 40 | 41 | fixture.detectChanges(); 42 | 43 | component.onUserChange(user); 44 | expect(spy).toHaveBeenCalledWith(action); 45 | }); 46 | }); 47 | -------------------------------------------------------------------------------- /src/app/users/containers/add/add.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { Store } from '@ngrx/store'; 3 | import * as fromRoot from '@state/index'; 4 | import { AddUser } from '@state/user/user.actions'; 5 | import { User } from '@state/user/user.model'; 6 | 7 | @Component({ 8 | templateUrl: './add.component.html', 9 | styleUrls: ['./add.component.scss'] 10 | }) 11 | export class AddComponent { 12 | constructor(private store: Store) {} 13 | 14 | onUserChange(user: User) { 15 | this.store.dispatch(new AddUser({ user: user })); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/app/users/containers/edit/__snapshots__/edit.component.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`EditComponent should match snapshot 1`] = ` 4 | 9 | 10 |
15 | 22 | 29 | 32 |
33 |
34 |
35 | `; 36 | -------------------------------------------------------------------------------- /src/app/users/containers/edit/edit.component.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/users/containers/edit/edit.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blove/ngrx-testing/5449c76871375ead6be7df7f080bcebba54c5c7c/src/app/users/containers/edit/edit.component.scss -------------------------------------------------------------------------------- /src/app/users/containers/edit/edit.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed, async } from '@angular/core/testing'; 2 | import { FormsModule, ReactiveFormsModule } from '@angular/forms'; 3 | import { ActivatedRoute, convertToParamMap } from '@angular/router'; 4 | import { RouterTestingModule } from '@angular/router/testing'; 5 | import { Store } from '@ngrx/store'; 6 | import * as fromRoot from '@state/index'; 7 | import { LoadUser, SelectUser, UpdateUser } from '@state/user/user.actions'; 8 | import { User, generateUser } from '@state/user/user.model'; 9 | import { hot } from 'jasmine-marbles'; 10 | import { BehaviorSubject } from 'rxjs'; 11 | import { UserFormComponent } from './../../components/user-form/user-form.component'; 12 | import { EditComponent } from './edit.component'; 13 | 14 | describe('EditComponent', () => { 15 | let component: EditComponent; 16 | let fixture: ComponentFixture; 17 | let store: Store; 18 | let user: User; 19 | 20 | beforeEach(() => { 21 | user = generateUser(); 22 | }); 23 | 24 | beforeEach(async(() => { 25 | TestBed.configureTestingModule({ 26 | declarations: [EditComponent, UserFormComponent], 27 | imports: [FormsModule, RouterTestingModule, ReactiveFormsModule], 28 | providers: [ 29 | { 30 | provide: ActivatedRoute, 31 | useValue: { 32 | paramMap: new BehaviorSubject( 33 | convertToParamMap({ 34 | id: user.id 35 | }) 36 | ) 37 | } 38 | }, 39 | { 40 | provide: Store, 41 | useValue: { 42 | dispatch: jest.fn(), 43 | pipe: jest.fn(() => hot('-a', { a: user })) 44 | } 45 | } 46 | ] 47 | }).compileComponents(); 48 | })); 49 | 50 | beforeEach(() => { 51 | fixture = TestBed.createComponent(EditComponent); 52 | component = fixture.componentInstance; 53 | store = TestBed.get(Store); 54 | }); 55 | 56 | it('should create', () => { 57 | expect(component).toBeTruthy(); 58 | }); 59 | 60 | it('should match snapshot', () => { 61 | fixture.detectChanges(); 62 | expect(fixture).toMatchSnapshot(); 63 | }); 64 | 65 | describe('ngOnInit', () => { 66 | it('should dispatch SelectUser action for specified id parameter', () => { 67 | const action = new SelectUser({ id: user.id }); 68 | const spy = jest.spyOn(store, 'dispatch'); 69 | 70 | fixture.detectChanges(); 71 | 72 | expect(spy).toHaveBeenCalledWith(action); 73 | }); 74 | 75 | it('should dispatch LoadUser action for specified id parameter', () => { 76 | const action = new LoadUser({ id: user.id }); 77 | const spy = jest.spyOn(store, 'dispatch'); 78 | 79 | fixture.detectChanges(); 80 | 81 | expect(spy).toHaveBeenCalledWith(action); 82 | }); 83 | }); 84 | 85 | describe('onUserChange', () => { 86 | it('should dispatch the UpdateUser action when onUserChange is invoked', () => { 87 | const updatedUser = generateUser(); 88 | const action = new UpdateUser({ user: updatedUser }); 89 | const spy = jest.spyOn(store, 'dispatch'); 90 | 91 | fixture.detectChanges(); 92 | 93 | component.onUserChange(updatedUser); 94 | expect(spy).toHaveBeenCalledWith(action); 95 | }); 96 | }); 97 | }); 98 | -------------------------------------------------------------------------------- /src/app/users/containers/edit/edit.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { ActivatedRoute } from '@angular/router'; 3 | import { Store, select } from '@ngrx/store'; 4 | import * as fromRoot from '@state/index'; 5 | import { selectSelectedUser } from '@state/user'; 6 | import { LoadUser, SelectUser, UpdateUser } from '@state/user/user.actions'; 7 | import { User } from '@state/user/user.model'; 8 | import { Observable } from 'rxjs'; 9 | import { filter, map, switchMap, tap } from 'rxjs/operators'; 10 | 11 | @Component({ 12 | templateUrl: './edit.component.html', 13 | styleUrls: ['./edit.component.scss'] 14 | }) 15 | export class EditComponent implements OnInit { 16 | user$: Observable; 17 | 18 | constructor( 19 | private activatedRoute: ActivatedRoute, 20 | private store: Store 21 | ) {} 22 | 23 | ngOnInit() { 24 | const PARAM_ID = 'id'; 25 | this.user$ = this.activatedRoute.paramMap.pipe( 26 | filter(paramMap => paramMap.has(PARAM_ID)), 27 | map(paramMap => paramMap.get(PARAM_ID)), 28 | tap(id => { 29 | this.store.dispatch(new SelectUser({ id: +id })); 30 | this.store.dispatch(new LoadUser({ id: +id })); 31 | }), 32 | switchMap(id => this.store.pipe(select(selectSelectedUser))) 33 | ); 34 | } 35 | 36 | onUserChange(user: User) { 37 | this.store.dispatch(new UpdateUser({ user: user })); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/app/users/containers/index/__snapshots__/index.component.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`IndexComponent should match snapshot 1`] = ` 4 | 9 | 16 |
    17 | 18 |
19 |
20 |
21 | `; 22 | -------------------------------------------------------------------------------- /src/app/users/containers/index/index.component.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/app/users/containers/index/index.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blove/ngrx-testing/5449c76871375ead6be7df7f080bcebba54c5c7c/src/app/users/containers/index/index.component.scss -------------------------------------------------------------------------------- /src/app/users/containers/index/index.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed, async } from '@angular/core/testing'; 2 | import { RouterTestingModule } from '@angular/router/testing'; 3 | import { Store } from '@ngrx/store'; 4 | import { LoadUsers } from '@state/user/user.actions'; 5 | import { generateUsers } from '@state/user/user.model'; 6 | import { cold, getTestScheduler, hot } from 'jasmine-marbles'; 7 | import { last } from 'rxjs/operators'; 8 | import { UserListComponent } from './../../components/user-list/user-list.component'; 9 | import { IndexComponent } from './index.component'; 10 | 11 | describe('IndexComponent', () => { 12 | let component: IndexComponent; 13 | let fixture: ComponentFixture; 14 | 15 | beforeEach(async(() => { 16 | TestBed.configureTestingModule({ 17 | declarations: [IndexComponent, UserListComponent], 18 | imports: [RouterTestingModule], 19 | providers: [ 20 | { 21 | provide: Store, 22 | useValue: { 23 | dispatch: jest.fn(), 24 | pipe: jest.fn() 25 | } 26 | } 27 | ] 28 | }).compileComponents(); 29 | })); 30 | 31 | beforeEach(() => { 32 | fixture = TestBed.createComponent(IndexComponent); 33 | component = fixture.componentInstance; 34 | }); 35 | 36 | it('should create', () => { 37 | expect(component).toBeTruthy(); 38 | }); 39 | 40 | it('should match snapshot', () => { 41 | fixture.detectChanges(); 42 | expect(fixture).toMatchSnapshot(); 43 | }); 44 | 45 | describe('ngOnInit()', () => { 46 | it('should dispatch an the LoadUsers action in ngOnInit lifecycle', () => { 47 | const action = new LoadUsers(); 48 | const store = TestBed.get(Store); 49 | const spy = jest.spyOn(store, 'dispatch'); 50 | 51 | fixture.detectChanges(); 52 | 53 | expect(spy).toHaveBeenCalledWith(action); 54 | }); 55 | 56 | it('should selectAllUsers', () => { 57 | const store = TestBed.get(Store); 58 | const users = generateUsers(); 59 | store.pipe = jest.fn(() => hot('-a', { a: users })); 60 | 61 | fixture.detectChanges(); 62 | 63 | const expected = cold('-a', { a: users }); 64 | expect(component.users).toBeObservable(expected); 65 | }); 66 | }); 67 | 68 | describe('users', () => { 69 | it('should be an observable of an array of user objects', done => { 70 | const users = generateUsers(); 71 | const store = TestBed.get(Store); 72 | store.pipe = jest.fn(() => cold('-a|', { a: users })); 73 | 74 | fixture.detectChanges(); 75 | 76 | component.users.pipe(last()).subscribe(componentUsers => { 77 | expect(componentUsers).toEqual(users); 78 | done(); 79 | }); 80 | 81 | getTestScheduler().flush(); 82 | }); 83 | }); 84 | }); 85 | -------------------------------------------------------------------------------- /src/app/users/containers/index/index.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { Router } from '@angular/router'; 3 | import { Store, select } from '@ngrx/store'; 4 | import { State } from '@state/index'; 5 | import { selectAllUsers } from '@state/user'; 6 | import { LoadUsers } from '@state/user/user.actions'; 7 | import { User } from '@state/user/user.model'; 8 | import { Observable } from 'rxjs'; 9 | 10 | @Component({ 11 | templateUrl: './index.component.html', 12 | styleUrls: ['./index.component.scss'] 13 | }) 14 | export class IndexComponent implements OnInit { 15 | users: Observable>; 16 | 17 | constructor(private router: Router, private store: Store) {} 18 | 19 | ngOnInit() { 20 | this.store.dispatch(new LoadUsers()); 21 | this.users = this.store.pipe(select(selectAllUsers)); 22 | } 23 | 24 | onSelectUser(user: User) { 25 | this.router.navigate(['/users', user.id]); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/app/users/routes.ts: -------------------------------------------------------------------------------- 1 | import { Routes } from '@angular/router'; 2 | import { AddComponent } from './containers/add/add.component'; 3 | import { EditComponent } from './containers/edit/edit.component'; 4 | import { IndexComponent } from './containers/index/index.component'; 5 | 6 | export const userRoutes: Routes = [ 7 | { 8 | path: '', 9 | component: IndexComponent 10 | }, 11 | { 12 | path: 'add', 13 | component: AddComponent 14 | }, 15 | { 16 | path: ':id', 17 | component: EditComponent 18 | } 19 | ]; 20 | -------------------------------------------------------------------------------- /src/app/users/users.module.spec.ts: -------------------------------------------------------------------------------- 1 | import { UsersModule } from './users.module'; 2 | 3 | describe('UsersModule', () => { 4 | let usersModule: UsersModule; 5 | 6 | beforeEach(() => { 7 | usersModule = new UsersModule(); 8 | }); 9 | 10 | it('should create an instance', () => { 11 | expect(usersModule).toBeTruthy(); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /src/app/users/users.module.ts: -------------------------------------------------------------------------------- 1 | import { CommonModule } from '@angular/common'; 2 | import { NgModule } from '@angular/core'; 3 | import { FormsModule, ReactiveFormsModule } from '@angular/forms'; 4 | import { RouterModule } from '@angular/router'; 5 | import { StateModule } from '@state/state.module'; 6 | import { UserFormComponent } from './components/user-form/user-form.component'; 7 | import { UserListComponent } from './components/user-list/user-list.component'; 8 | import { AddComponent } from './containers/add/add.component'; 9 | import { EditComponent } from './containers/edit/edit.component'; 10 | import { IndexComponent } from './containers/index/index.component'; 11 | 12 | @NgModule({ 13 | imports: [ 14 | CommonModule, 15 | FormsModule, 16 | ReactiveFormsModule, 17 | RouterModule, 18 | StateModule 19 | ], 20 | declarations: [ 21 | IndexComponent, 22 | UserListComponent, 23 | AddComponent, 24 | UserFormComponent, 25 | EditComponent 26 | ] 27 | }) 28 | export class UsersModule {} 29 | -------------------------------------------------------------------------------- /src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blove/ngrx-testing/5449c76871375ead6be7df7f080bcebba54c5c7c/src/assets/.gitkeep -------------------------------------------------------------------------------- /src/browserslist: -------------------------------------------------------------------------------- 1 | # This file is currently used by autoprefixer to adjust CSS to support the below specified browsers 2 | # For additional information regarding the format and rule options, please see: 3 | # https://github.com/browserslist/browserslist#queries 4 | # For IE 9-11 support, please uncomment the last line of the file and adjust as needed 5 | > 0.5% 6 | last 2 versions 7 | Firefox ESR 8 | not dead 9 | # IE 9-11 -------------------------------------------------------------------------------- /src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true 3 | }; 4 | -------------------------------------------------------------------------------- /src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // This file can be replaced during build by using the `fileReplacements` array. 2 | // `ng build ---prod` replaces `environment.ts` with `environment.prod.ts`. 3 | // The list of file replacements can be found in `angular.json`. 4 | 5 | export const environment = { 6 | production: false 7 | }; 8 | 9 | /* 10 | * In development mode, to ignore zone related error stack frames such as 11 | * `zone.run`, `zoneDelegate.invokeTask` for easier debugging, you can 12 | * import the following file, but please comment it out in production mode 13 | * because it will have performance impact when throw error 14 | */ 15 | // import 'zone.js/dist/zone-error'; // Included with Angular CLI. 16 | -------------------------------------------------------------------------------- /src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blove/ngrx-testing/5449c76871375ead6be7df7f080bcebba54c5c7c/src/favicon.ico -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NgrxMarbles 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/jestGlobalMocks.ts: -------------------------------------------------------------------------------- 1 | // ng s is complaining about global, so doing this "hackishly" 2 | declare var global; 3 | global['CSS'] = null; 4 | 5 | const mock = () => { 6 | let storage = {}; 7 | return { 8 | getItem: key => (key in storage ? storage[key] : null), 9 | setItem: (key, value) => (storage[key] = value || ''), 10 | removeItem: key => delete storage[key], 11 | clear: () => (storage = {}) 12 | }; 13 | }; 14 | 15 | Object.defineProperty(window, 'localStorage', { value: mock() }); 16 | Object.defineProperty(window, 'sessionStorage', { value: mock() }); 17 | Object.defineProperty(document, 'doctype', { 18 | value: '' 19 | }); 20 | Object.defineProperty(window, 'getComputedStyle', { 21 | value: () => { 22 | return { 23 | display: 'none', 24 | appearance: ['-webkit-appearance'] 25 | }; 26 | } 27 | }); 28 | /** 29 | * ISSUE: https://github.com/angular/material2/issues/7101 30 | * Workaround for JSDOM missing transform property 31 | */ 32 | Object.defineProperty(document.body.style, 'transform', { 33 | value: () => { 34 | return { 35 | enumerable: true, 36 | configurable: true 37 | }; 38 | } 39 | }); 40 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { enableProdMode } from '@angular/core'; 2 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 3 | 4 | import { AppModule } from './app/app.module'; 5 | import { environment } from './environments/environment'; 6 | 7 | if (environment.production) { 8 | enableProdMode(); 9 | } 10 | 11 | platformBrowserDynamic().bootstrapModule(AppModule) 12 | .catch(err => console.log(err)); 13 | -------------------------------------------------------------------------------- /src/polyfills.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file includes polyfills needed by Angular and is loaded before the app. 3 | * You can add your own extra polyfills to this file. 4 | * 5 | * This file is divided into 2 sections: 6 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. 7 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main 8 | * file. 9 | * 10 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that 11 | * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera), 12 | * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile. 13 | * 14 | * Learn more in https://angular.io/docs/ts/latest/guide/browser-support.html 15 | */ 16 | 17 | /*************************************************************************************************** 18 | * BROWSER POLYFILLS 19 | */ 20 | 21 | /** IE9, IE10 and IE11 requires all of the following polyfills. **/ 22 | // import 'core-js/es6/symbol'; 23 | // import 'core-js/es6/object'; 24 | // import 'core-js/es6/function'; 25 | // import 'core-js/es6/parse-int'; 26 | // import 'core-js/es6/parse-float'; 27 | // import 'core-js/es6/number'; 28 | // import 'core-js/es6/math'; 29 | // import 'core-js/es6/string'; 30 | // import 'core-js/es6/date'; 31 | // import 'core-js/es6/array'; 32 | // import 'core-js/es6/regexp'; 33 | // import 'core-js/es6/map'; 34 | // import 'core-js/es6/weak-map'; 35 | // import 'core-js/es6/set'; 36 | 37 | /** IE10 and IE11 requires the following for NgClass support on SVG elements */ 38 | // import 'classlist.js'; // Run `npm install --save classlist.js`. 39 | 40 | /** IE10 and IE11 requires the following for the Reflect API. */ 41 | // import 'core-js/es6/reflect'; 42 | 43 | 44 | /** Evergreen browsers require these. **/ 45 | // Used for reflect-metadata in JIT. If you use AOT (and only Angular decorators), you can remove. 46 | import 'core-js/es7/reflect'; 47 | 48 | 49 | /** 50 | * Web Animations `@angular/platform-browser/animations` 51 | * Only required if AnimationBuilder is used within the application and using IE/Edge or Safari. 52 | * Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0). 53 | **/ 54 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`. 55 | 56 | /** 57 | * By default, zone.js will patch all possible macroTask and DomEvents 58 | * user can disable parts of macroTask/DomEvents patch by setting following flags 59 | */ 60 | 61 | // (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame 62 | // (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick 63 | // (window as any).__zone_symbol__BLACK_LISTED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames 64 | 65 | /* 66 | * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js 67 | * with the following flag, it will bypass `zone.js` patch for IE/Edge 68 | */ 69 | // (window as any).__Zone_enable_cross_context_check = true; 70 | 71 | /*************************************************************************************************** 72 | * Zone JS is required by default for Angular itself. 73 | */ 74 | import 'zone.js/dist/zone'; // Included with Angular CLI. 75 | 76 | 77 | 78 | /*************************************************************************************************** 79 | * APPLICATION IMPORTS 80 | */ 81 | -------------------------------------------------------------------------------- /src/setup-jest.ts: -------------------------------------------------------------------------------- 1 | import 'jest-preset-angular'; 2 | 3 | import './jestGlobalMocks'; 4 | -------------------------------------------------------------------------------- /src/setupJest.ts: -------------------------------------------------------------------------------- 1 | import 'jest-preset-angular'; 2 | import './jestGlobalMocks'; 3 | -------------------------------------------------------------------------------- /src/styles.scss: -------------------------------------------------------------------------------- 1 | /* You can add global styles to this file, and also import other style files */ 2 | -------------------------------------------------------------------------------- /src/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/app", 5 | "module": "es2015", 6 | "types": [] 7 | }, 8 | "exclude": [ 9 | "src/test.ts", 10 | "**/*.spec.ts" 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /src/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/spec", 5 | "module": "commonjs", 6 | "types": [ 7 | "jasmine", 8 | "jest", 9 | "node" 10 | ] 11 | }, 12 | "files": [ 13 | "test.ts", 14 | "polyfills.ts" 15 | ], 16 | "include": [ 17 | "**/*.spec.ts", 18 | "**/*.d.ts" 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /src/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tslint.json", 3 | "rules": { 4 | "directive-selector": [ 5 | true, 6 | "attribute", 7 | "app", 8 | "camelCase" 9 | ], 10 | "component-selector": [ 11 | true, 12 | "element", 13 | "app", 14 | "kebab-case" 15 | ] 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "baseUrl": "./", 5 | "outDir": "./dist/out-tsc", 6 | "sourceMap": true, 7 | "declaration": false, 8 | "moduleResolution": "node", 9 | "emitDecoratorMetadata": true, 10 | "experimentalDecorators": true, 11 | "target": "es5", 12 | "typeRoots": [ 13 | "node_modules/@types" 14 | ], 15 | "lib": [ 16 | "es2017", 17 | "dom" 18 | ], 19 | "paths": { 20 | "@core/*": ["src/app/core/*"], 21 | "@state/*": ["src/app/state/*"], 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rulesDirectory": [ 3 | "node_modules/codelyzer" 4 | ], 5 | "rules": { 6 | "arrow-return-shorthand": true, 7 | "callable-types": true, 8 | "class-name": true, 9 | "comment-format": [ 10 | true, 11 | "check-space" 12 | ], 13 | "curly": true, 14 | "deprecation": { 15 | "severity": "warn" 16 | }, 17 | "eofline": true, 18 | "forin": true, 19 | "import-blacklist": [ 20 | true, 21 | "rxjs/Rx" 22 | ], 23 | "import-spacing": true, 24 | "indent": [ 25 | true, 26 | "spaces" 27 | ], 28 | "interface-over-type-literal": true, 29 | "label-position": true, 30 | "max-line-length": [ 31 | true, 32 | 140 33 | ], 34 | "member-access": false, 35 | "member-ordering": [ 36 | true, 37 | { 38 | "order": [ 39 | "static-field", 40 | "instance-field", 41 | "static-method", 42 | "instance-method" 43 | ] 44 | } 45 | ], 46 | "no-arg": true, 47 | "no-bitwise": true, 48 | "no-console": [ 49 | true, 50 | "debug", 51 | "info", 52 | "time", 53 | "timeEnd", 54 | "trace" 55 | ], 56 | "no-construct": true, 57 | "no-debugger": true, 58 | "no-duplicate-super": true, 59 | "no-empty": false, 60 | "no-empty-interface": true, 61 | "no-eval": true, 62 | "no-inferrable-types": [ 63 | true, 64 | "ignore-params" 65 | ], 66 | "no-misused-new": true, 67 | "no-non-null-assertion": true, 68 | "no-shadowed-variable": true, 69 | "no-string-literal": false, 70 | "no-string-throw": true, 71 | "no-switch-case-fall-through": true, 72 | "no-trailing-whitespace": true, 73 | "no-unnecessary-initializer": true, 74 | "no-unused-expression": true, 75 | "no-use-before-declare": true, 76 | "no-var-keyword": true, 77 | "object-literal-sort-keys": false, 78 | "one-line": [ 79 | true, 80 | "check-open-brace", 81 | "check-catch", 82 | "check-else", 83 | "check-whitespace" 84 | ], 85 | "prefer-const": true, 86 | "quotemark": [ 87 | true, 88 | "single" 89 | ], 90 | "radix": true, 91 | "semicolon": [ 92 | true, 93 | "always" 94 | ], 95 | "triple-equals": [ 96 | true, 97 | "allow-null-check" 98 | ], 99 | "typedef-whitespace": [ 100 | true, 101 | { 102 | "call-signature": "nospace", 103 | "index-signature": "nospace", 104 | "parameter": "nospace", 105 | "property-declaration": "nospace", 106 | "variable-declaration": "nospace" 107 | } 108 | ], 109 | "unified-signatures": true, 110 | "variable-name": false, 111 | "whitespace": [ 112 | true, 113 | "check-branch", 114 | "check-decl", 115 | "check-operator", 116 | "check-separator", 117 | "check-type" 118 | ], 119 | "no-output-on-prefix": true, 120 | "use-input-property-decorator": true, 121 | "use-output-property-decorator": true, 122 | "use-host-property-decorator": true, 123 | "no-input-rename": true, 124 | "no-output-rename": true, 125 | "use-life-cycle-interface": true, 126 | "use-pipe-transform-interface": true, 127 | "component-class-suffix": true, 128 | "directive-class-suffix": true 129 | } 130 | } 131 | --------------------------------------------------------------------------------