diff --git a/package-lock.json b/package-lock.json index 613444f..22e0b9b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -614,8 +614,7 @@ "ansi-regex": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", - "dev": true + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" }, "ansi-styles": { "version": "3.2.1", @@ -626,6 +625,11 @@ "color-convert": "^1.9.0" } }, + "ansicolors": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/ansicolors/-/ansicolors-0.2.1.tgz", + "integrity": "sha1-vgiVmQl7dKXJxKhKDNvNtivYeu8=" + }, "anymatch": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz", @@ -1596,6 +1600,15 @@ "integrity": "sha512-gJZIfmkuy84agOeAZc7WJOexZhisZaBSFk96gkGM6TkH7+1mBfr/MSPnXC8lO0g7guh/ucbswYjruvDbzc6i0g==", "dev": true }, + "cardinal": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/cardinal/-/cardinal-1.0.0.tgz", + "integrity": "sha1-UOIcGwqjdyn5N33vGWtanOyTLuk=", + "requires": { + "ansicolors": "~0.2.1", + "redeyed": "~1.0.0" + } + }, "caseless": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", @@ -1727,6 +1740,30 @@ "source-map": "0.5.x" } }, + "cli-table": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/cli-table/-/cli-table-0.3.1.tgz", + "integrity": "sha1-9TsFJmqLGguTSz0IIebi3FkUriM=", + "requires": { + "colors": "1.0.3" + }, + "dependencies": { + "colors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.0.3.tgz", + "integrity": "sha1-BDP0TYCWgP3rYO0mDxsMJi6CpAs=" + } + } + }, + "cli-usage": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/cli-usage/-/cli-usage-0.1.7.tgz", + "integrity": "sha512-x/Q52iLSZsRrRb2ePmTsVYXrGcrPQ8G4yRAY7QpMlumxAfPVrnDOH2X6Z5s8qsAX7AA7YuIi8AXFrvH0wWEesA==", + "requires": { + "marked": "^0.3.12", + "marked-terminal": "^2.0.0" + } + }, "cliui": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/cliui/-/cliui-2.1.0.tgz", @@ -2909,8 +2946,7 @@ "escape-string-regexp": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", - "dev": true + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" }, "escodegen": { "version": "1.8.1", @@ -4452,6 +4488,11 @@ "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=", "dev": true }, + "growly": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/growly/-/growly-1.3.0.tgz", + "integrity": "sha1-8QdIy+dq+WS3yWyTxrzCivEgwIE=" + }, "handle-thing": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-1.2.5.tgz", @@ -4545,7 +4586,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", - "dev": true, "requires": { "ansi-regex": "^2.0.0" } @@ -5393,8 +5433,7 @@ "isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", - "dev": true + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=" }, "isobject": { "version": "3.0.1", @@ -6008,6 +6047,14 @@ "integrity": "sha512-uhNED+4B1axgptXkM8cCa3kztpQqsPrOxhfbjr4FdunNexnU6+cF2bfiIeGfsFMhphVyOMKy/S9LFaOFj8VXRA==", "dev": true }, + "karma-notify-reporter": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/karma-notify-reporter/-/karma-notify-reporter-1.0.1.tgz", + "integrity": "sha1-2b+0UrxTU2cUO25gMl3UAPSfIMg=", + "requires": { + "node-notifier": "^4.5.0" + } + }, "karma-source-map-support": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/karma-source-map-support/-/karma-source-map-support-1.3.0.tgz", @@ -6194,11 +6241,62 @@ "integrity": "sha512-UejweD1pDoXu+AD825lWwp4ZGtSwgnpZxb3JDViD7StjQz+Nb/6l093lx4OQ0foGWNRoc19mWy7BzL+UAK2iVg==", "dev": true }, + "lodash._arraycopy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/lodash._arraycopy/-/lodash._arraycopy-3.0.0.tgz", + "integrity": "sha1-due3wfH7klRzdIeKVi7Qaj5Q9uE=" + }, + "lodash._arrayeach": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/lodash._arrayeach/-/lodash._arrayeach-3.0.0.tgz", + "integrity": "sha1-urFWsqkNPxu9XGU0AzSeXlkz754=" + }, + "lodash._baseassign": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/lodash._baseassign/-/lodash._baseassign-3.2.0.tgz", + "integrity": "sha1-jDigmVAPIVrQnlnxci/QxSv+Ck4=", + "requires": { + "lodash._basecopy": "^3.0.0", + "lodash.keys": "^3.0.0" + } + }, + "lodash._baseclone": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lodash._baseclone/-/lodash._baseclone-3.3.0.tgz", + "integrity": "sha1-MDUZv2OT/n5C802LYw73eU41Qrc=", + "requires": { + "lodash._arraycopy": "^3.0.0", + "lodash._arrayeach": "^3.0.0", + "lodash._baseassign": "^3.0.0", + "lodash._basefor": "^3.0.0", + "lodash.isarray": "^3.0.0", + "lodash.keys": "^3.0.0" + } + }, + "lodash._basecopy": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/lodash._basecopy/-/lodash._basecopy-3.0.1.tgz", + "integrity": "sha1-jaDmqHbPNEwK2KVIghEd08XHyjY=" + }, + "lodash._basefor": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash._basefor/-/lodash._basefor-3.0.3.tgz", + "integrity": "sha1-dVC06SGO8J+tJDQ7YSAhx5tMIMI=" + }, + "lodash._bindcallback": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/lodash._bindcallback/-/lodash._bindcallback-3.0.1.tgz", + "integrity": "sha1-5THCdkTPi1epnhftlbNcdIeJOS4=" + }, + "lodash._getnative": { + "version": "3.9.1", + "resolved": "https://registry.npmjs.org/lodash._getnative/-/lodash._getnative-3.9.1.tgz", + "integrity": "sha1-VwvH3t5G1hzc3mh9ZdPuy6o6r/U=" + }, "lodash.assign": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/lodash.assign/-/lodash.assign-4.2.0.tgz", - "integrity": "sha1-DZnzzNem0mHRm9rrkkUAXShYCOc=", - "dev": true + "integrity": "sha1-DZnzzNem0mHRm9rrkkUAXShYCOc=" }, "lodash.clonedeep": { "version": "4.5.0", @@ -6206,6 +6304,26 @@ "integrity": "sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=", "dev": true }, + "lodash.isarguments": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", + "integrity": "sha1-L1c9hcaiQon/AGY7SRwdM4/zRYo=" + }, + "lodash.isarray": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/lodash.isarray/-/lodash.isarray-3.0.4.tgz", + "integrity": "sha1-eeTriMNqgSKvhvhEqpvNhRtfu1U=" + }, + "lodash.keys": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/lodash.keys/-/lodash.keys-3.1.2.tgz", + "integrity": "sha1-TbwEcrFWvlCgsoaFXRvQsMZWCYo=", + "requires": { + "lodash._getnative": "^3.0.0", + "lodash.isarguments": "^3.0.0", + "lodash.isarray": "^3.0.0" + } + }, "lodash.mergewith": { "version": "4.6.1", "resolved": "https://registry.npmjs.org/lodash.mergewith/-/lodash.mergewith-4.6.1.tgz", @@ -6218,6 +6336,11 @@ "integrity": "sha1-0jM6NtnncXyK0vfKyv7HwytERmQ=", "dev": true }, + "lodash.toarray": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.toarray/-/lodash.toarray-4.4.0.tgz", + "integrity": "sha1-JMS/zWsvuji/0FlNsRedjptlZWE=" + }, "log-symbols": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-2.2.0.tgz", @@ -6602,6 +6725,47 @@ "object-visit": "^1.0.0" } }, + "marked": { + "version": "0.3.19", + "resolved": "https://registry.npmjs.org/marked/-/marked-0.3.19.tgz", + "integrity": "sha512-ea2eGWOqNxPcXv8dyERdSr/6FmzvWwzjMxpfGB/sbMccXoct+xY+YukPD+QTUZwyvK7BZwcr4m21WBOW41pAkg==" + }, + "marked-terminal": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/marked-terminal/-/marked-terminal-2.0.0.tgz", + "integrity": "sha1-Xq9Wi+ZvaGVBr6UqVYKAMQox3i0=", + "requires": { + "cardinal": "^1.0.0", + "chalk": "^1.1.3", + "cli-table": "^0.3.1", + "lodash.assign": "^4.2.0", + "node-emoji": "^1.4.1" + }, + "dependencies": { + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=" + }, + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "requires": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + } + }, + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=" + } + } + }, "math-random": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/math-random/-/math-random-1.0.1.tgz", @@ -6938,6 +7102,14 @@ "lower-case": "^1.1.1" } }, + "node-emoji": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/node-emoji/-/node-emoji-1.8.1.tgz", + "integrity": "sha512-+ktMAh1Jwas+TnGodfCfjUbJKoANqPaJFN0z0iqh41eqD8dvguNzcitVSBSVK1pidz0AqGbLKcoVuVLRVZ/aVg==", + "requires": { + "lodash.toarray": "^4.4.0" + } + }, "node-forge": { "version": "0.7.5", "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.7.5.tgz", @@ -7012,6 +7184,36 @@ } } }, + "node-notifier": { + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/node-notifier/-/node-notifier-4.6.1.tgz", + "integrity": "sha1-BW0UJE89zBzq3+aK+c/wxUc6M/M=", + "requires": { + "cli-usage": "^0.1.1", + "growly": "^1.2.0", + "lodash.clonedeep": "^3.0.0", + "minimist": "^1.1.1", + "semver": "^5.1.0", + "shellwords": "^0.1.0", + "which": "^1.0.5" + }, + "dependencies": { + "lodash.clonedeep": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-3.0.2.tgz", + "integrity": "sha1-oKHkDYKl6on/WxR7hETtY9koJ9s=", + "requires": { + "lodash._baseclone": "^3.0.0", + "lodash._bindcallback": "^3.0.0" + } + }, + "minimist": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=" + } + } + }, "node-sass": { "version": "4.9.0", "resolved": "https://registry.npmjs.org/node-sass/-/node-sass-4.9.0.tgz", @@ -8622,6 +8824,21 @@ "strip-indent": "^1.0.1" } }, + "redeyed": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/redeyed/-/redeyed-1.0.1.tgz", + "integrity": "sha1-6WwZO0DAgWsArshCaY5hGF5VSYo=", + "requires": { + "esprima": "~3.0.0" + }, + "dependencies": { + "esprima": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-3.0.0.tgz", + "integrity": "sha1-U88kes2ncxPlUcOqLnM0LT+099k=" + } + } + }, "redis": { "version": "2.8.0", "resolved": "https://registry.npmjs.org/redis/-/redis-2.8.0.tgz", @@ -9110,8 +9327,7 @@ "semver": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/semver/-/semver-5.5.0.tgz", - "integrity": "sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA==", - "dev": true + "integrity": "sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA==" }, "semver-dsl": { "version": "1.0.1", @@ -9284,6 +9500,11 @@ "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", "dev": true }, + "shellwords": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/shellwords/-/shellwords-0.1.1.tgz", + "integrity": "sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww==" + }, "signal-exit": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", @@ -9864,7 +10085,6 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", - "dev": true, "requires": { "ansi-regex": "^2.0.0" } @@ -11132,7 +11352,6 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/which/-/which-1.3.0.tgz", "integrity": "sha512-xcJpopdamTuY5duC/KnTTNBraPK54YwpenP4lzxU8H91GudWpFv38u0CKjclE1Wi2EH2EDz5LRcHcKbCIzqGyg==", - "dev": true, "requires": { "isexe": "^2.0.0" } diff --git a/package.json b/package.json index 938f19c..2145047 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "@angular/router": "^6.0.1", "angular-in-memory-web-api": "^0.6.0", "core-js": "^2.4.1", + "karma-notify-reporter": "^1.0.1", "rxjs": "^6.1.0", "zone.js": "^0.8.19" }, diff --git a/src/app/hero.service.spec.ts b/src/app/hero.service.spec.ts index 1fca8d4..3809100 100644 --- a/src/app/hero.service.spec.ts +++ b/src/app/hero.service.spec.ts @@ -1,10 +1,21 @@ import { TestBed, inject } from '@angular/core/testing'; -import { HttpClientModule } from '@angular/common/http'; +import { HttpClientModule, HttpErrorResponse } from '@angular/common/http'; +import { asyncData, asyncError } from '../testing/async-observable-helpers'; +import { Hero } from './hero'; import { HeroService } from './hero.service'; import { MessageService } from './message.service'; describe('HeroService', () => { + let httpClientSpy: { + get: jasmine.Spy, + put: jasmine.Spy, + post: jasmine.Spy, + delete: jasmine.Spy + }; + let messageServiceSpy: { add: jasmine.Spy }; + let heroService: HeroService; + beforeEach(() => { TestBed.configureTestingModule({ providers: [ @@ -15,9 +26,276 @@ describe('HeroService', () => { HttpClientModule ], }); - }); + httpClientSpy = jasmine.createSpyObj('HttpClient', ['get', 'put', 'post', 'delete']); + messageServiceSpy = jasmine.createSpyObj('MessageService', ['add']); + heroService = new HeroService( httpClientSpy, messageServiceSpy); + }); it('should be created', inject([HeroService], (service: HeroService) => { - expect(service).toBeTruthy(); + expect(heroService).toBeTruthy(); })); + + describe('#getHeroes', () => { + it('should fetch GET /api/heroes', () => { + httpClientSpy.get.and.returnValue(asyncData([])); + heroService.getHeroes(); + expect(httpClientSpy.get.calls.allArgs()).toEqual([['api/heroes']]); + }); + + describe('when GET /api/heroes returns 200 OK', () => { + const heroes: Hero[] = [{ id: 1, name: 'A' }, { id: 2, name: 'B' }]; + + beforeEach(() => { + httpClientSpy.get.and.returnValue(asyncData(heroes)); + }); + + it('should return expected heroes', async () => { + const response = await heroService.getHeroes().toPromise(); + expect(response).toEqual(heroes); + }); + + it('should add a message to the message service', async () => { + await heroService.getHeroes().toPromise(); + expect(messageServiceSpy.add.calls.allArgs()).toEqual([['HeroService: fetched heroes']]); + }); + }); + + describe('when GET /api/heroes returns 404 Not Found', () => { + beforeEach(() => { + const errorResponse = new HttpErrorResponse({ status: 404, statusText: 'Not Found' }); + httpClientSpy.get.and.returnValue(asyncError(errorResponse)); + }); + + it('should return an empty list of heroes', async () => { + const response = await heroService.getHeroes().toPromise(); + expect(response).toEqual([]); + }); + + it('should add a message to the message service', async () => { + const errorMessage = 'HeroService: getHeroes failed: Http failure response for (unknown url): 404 Not Found'; + await heroService.getHeroes().toPromise(); + expect(messageServiceSpy.add.calls.allArgs()).toEqual([[errorMessage]]); + }); + }); + }); + + describe('#getHero', () => { + it('should fetch GET /api/heroes/{id}', () => { + httpClientSpy.get.and.returnValue(asyncData({})); + heroService.getHero(1); + expect(httpClientSpy.get.calls.allArgs()).toEqual([['api/heroes/1']]); + }); + + describe('when GET /api/heroes/{id} returns 200 OK', () => { + const hero: Hero = { id: 1, name: 'A' }; + + beforeEach(() => { + httpClientSpy.get.and.returnValue(asyncData(hero)); + }); + + it('should return expected hero', async () => { + const response = await heroService.getHero(1).toPromise(); + expect(response).toEqual(hero); + }); + + it('should add a message to the message service', async () => { + await heroService.getHero(1).toPromise(); + expect(messageServiceSpy.add.calls.allArgs()).toEqual([['HeroService: fetched hero id=1']]); + }); + }); + + describe('when GET /api/heroes/{id} returns 404 Not Found', () => { + beforeEach(() => { + const errorResponse = new HttpErrorResponse({ status: 404, statusText: 'Not Found' }); + httpClientSpy.get.and.returnValue(asyncError(errorResponse)); + }); + + it('should return undefined', async () => { + const response = await heroService.getHero(1).toPromise(); + expect(response).toBe(undefined); + }); + + it('should add a message to the message service', async () => { + const errorMessage = 'HeroService: getHero id=1 failed: Http failure response for (unknown url): 404 Not Found'; + await heroService.getHero(1).toPromise(); + expect(messageServiceSpy.add.calls.allArgs()).toEqual([[errorMessage]]); + }); + }); + }); + + describe('#updateHero', () => { + const hero: Hero = { id: 1, name: 'A' }; + + it('should call PUT /api/heroes/{id}', () => { + httpClientSpy.put.and.returnValue(asyncData({})); + heroService.updateHero(hero); + expect(httpClientSpy.put.calls.allArgs()).toEqual([['api/heroes', hero, jasmine.any(Object)]]); + }); + + describe('when PUT /api/heroes/{id} returns 200 OK', () => { + beforeEach(() => { + httpClientSpy.put.and.returnValue(asyncData(hero)); + }); + + it('should return the updated hero', async () => { + const response = await heroService.updateHero(hero).toPromise(); + expect(response).toEqual(hero); + }); + + it('should add a message to the message service', async () => { + await heroService.updateHero(hero).toPromise(); + expect(messageServiceSpy.add.calls.allArgs()).toEqual([['HeroService: updated hero id=1']]); + }); + }); + + describe('when PUT /api/heroes/{id} returns 404 Not Found', () => { + beforeEach(() => { + const errorResponse = new HttpErrorResponse({ status: 404, statusText: 'Not Found' }); + httpClientSpy.put.and.returnValue(asyncError(errorResponse)); + }); + + it('should return undefined', async () => { + const response = await heroService.updateHero(hero).toPromise(); + expect(response).toBe(undefined); + }); + + it('should add a message to the message service', async () => { + const errorMessage = 'HeroService: updateHero failed: Http failure response for (unknown url): 404 Not Found'; + await heroService.updateHero(hero).toPromise(); + expect(messageServiceSpy.add.calls.allArgs()).toEqual([[errorMessage]]); + }); + }); + }); + + describe('#addHero', () => { + const hero: Hero = { id: 1, name: 'A' }; + + it('should call POST /api/heroes', () => { + httpClientSpy.post.and.returnValue(asyncData({})); + heroService.addHero(hero); + expect(httpClientSpy.post.calls.allArgs()).toEqual([['api/heroes', hero, jasmine.any(Object)]]); + }); + + describe('when POST /api/heroes returns 201 Created', () => { + beforeEach(() => { + httpClientSpy.post.and.returnValue(asyncData(hero)); + }); + + it('should return the added hero', async () => { + const response = await heroService.addHero(hero).toPromise(); + expect(response).toEqual(hero); + }); + + it('should add a message to the message service', async () => { + await heroService.addHero(hero).toPromise(); + expect(messageServiceSpy.add.calls.allArgs()).toEqual([['HeroService: added hero w/ id=1']]); + }); + }); + + describe('when POST /api/heroes returns 403 Forbidden', () => { + beforeEach(() => { + const errorResponse = new HttpErrorResponse({ status: 403, statusText: 'Forbidden' }); + httpClientSpy.post.and.returnValue(asyncError(errorResponse)); + }); + + it('should return undefined', async () => { + const response = await heroService.addHero(hero).toPromise(); + expect(response).toBe(undefined); + }); + + it('should add a message to the message service', async () => { + const errorMessage = 'HeroService: addHero failed: Http failure response for (unknown url): 403 Forbidden'; + await heroService.addHero(hero).toPromise(); + expect(messageServiceSpy.add.calls.allArgs()).toEqual([[errorMessage]]); + }); + }); + }); + + describe('#deleteHero', () => { + const hero: Hero = { id: 1, name: 'A' }; + + it('should call DELETE /api/heroes/{id}', () => { + httpClientSpy.delete.and.returnValue(asyncData({})); + heroService.deleteHero(hero); + expect(httpClientSpy.delete.calls.allArgs()).toEqual([['api/heroes/1', jasmine.any(Object)]]); + }); + + describe('when DELETE /api/heroes/{id} returns 204 No Content', () => { + beforeEach(() => { + httpClientSpy.delete.and.returnValue(asyncData(undefined)); + }); + + it('should return undefined', async () => { + const response = await heroService.deleteHero(hero).toPromise(); + expect(response).toBe(undefined); + }); + + it('should add a message to the message service', async () => { + await heroService.deleteHero(hero).toPromise(); + expect(messageServiceSpy.add.calls.allArgs()).toEqual([['HeroService: deleted hero id=1']]); + }); + }); + + describe('when DELETE /api/heroes/{id} returns 404 Not Found', () => { + beforeEach(() => { + const errorResponse = new HttpErrorResponse({ status: 404, statusText: 'Not Found' }); + httpClientSpy.delete.and.returnValue(asyncError(errorResponse)); + }); + + it('should return undefined', async () => { + const response = await heroService.deleteHero(hero).toPromise(); + expect(response).toBe(undefined); + }); + + it('should add a message to the message service', async () => { + const errorMessage = 'HeroService: deleteHero failed: Http failure response for (unknown url): 404 Not Found'; + await heroService.deleteHero(hero).toPromise(); + expect(messageServiceSpy.add.calls.allArgs()).toEqual([[errorMessage]]); + }); + }); + }); + + describe('#searchHeroes', () => { + it('should call GET /api/heroes/?name={query}', () => { + httpClientSpy.get.and.returnValue(asyncData({})); + heroService.searchHeroes('foo'); + expect(httpClientSpy.get.calls.allArgs()).toEqual([['api/heroes/?name=foo']]); + }); + + describe('when GET /api/heroes/?name={query} returns 200 OK', () => { + const heroes: Hero[] = [{ id: 1, name: 'fooA' }, { id: 2, name: 'fooB' }]; + + beforeEach(() => { + httpClientSpy.get.and.returnValue(asyncData(heroes)); + }); + + it('should return expected heroes', async () => { + const response = await heroService.searchHeroes('foo').toPromise(); + expect(response).toEqual(heroes); + }); + + it('should add a message to the message service', async () => { + await heroService.searchHeroes('foo').toPromise(); + expect(messageServiceSpy.add.calls.allArgs()).toEqual([['HeroService: found heroes matching "foo"']]); + }); + }); + + describe('when GET /api/heroes/?name={query} returns 404 Not Found', () => { + beforeEach(() => { + const errorResponse = new HttpErrorResponse({ status: 404, statusText: 'Not Found' }); + httpClientSpy.get.and.returnValue(asyncError(errorResponse)); + }); + + it('should return an empty list of heroes', async () => { + const response = await heroService.searchHeroes('foo').toPromise(); + expect(response).toEqual([]); + }); + + it('should add a message to the message service', async () => { + const errorMessage = 'HeroService: searchHeroes failed: Http failure response for (unknown url): 404 Not Found'; + await heroService.searchHeroes('foo').toPromise(); + expect(messageServiceSpy.add.calls.allArgs()).toEqual([[errorMessage]]); + }); + }); + }); }); diff --git a/src/app/hero.service.ts b/src/app/hero.service.ts index 832a976..873f2e3 100644 --- a/src/app/hero.service.ts +++ b/src/app/hero.service.ts @@ -62,7 +62,6 @@ export class HeroService { deleteHero (hero: Hero | number): Observable { const id = typeof hero === 'number' ? hero : hero.id; const url = `${this.heroesUrl}/${id}`; - return this.http.delete(url, httpOptions).pipe( tap(_ => this.log(`deleted hero id=${id}`)), catchError(this.handleError('deleteHero')) diff --git a/src/app/message.service.spec.ts b/src/app/message.service.spec.ts index 63ecfd8..e0446f0 100644 --- a/src/app/message.service.spec.ts +++ b/src/app/message.service.spec.ts @@ -12,4 +12,25 @@ describe('MessageService', () => { it('should be created', inject([MessageService], (service: MessageService) => { expect(service).toBeTruthy(); })); + + it('should be created with an empty messages list', inject([MessageService], (service: MessageService) => { + expect(service.messages).toEqual([]); + })); + + describe('#add', () => { + it('should add the given message to the list', inject([MessageService], (service: MessageService) => { + service.add('foo'); + service.add('bar'); + expect(service.messages).toEqual(['foo', 'bar']); + })); + }); + + describe('#clear', () => { + it('should empty the message list', inject([MessageService], (service: MessageService) => { + service.add('foo'); + service.add('bar'); + service.clear(); + expect(service.messages).toEqual([]); + })); + }); }); diff --git a/src/app/messages/messages.component.html b/src/app/messages/messages.component.html index fe5fdee..9d24da4 100644 --- a/src/app/messages/messages.component.html +++ b/src/app/messages/messages.component.html @@ -2,6 +2,6 @@

Messages

-
{{message}}
+
{{message}}
diff --git a/src/app/messages/messages.component.spec.ts b/src/app/messages/messages.component.spec.ts index 24eda91..2f75495 100644 --- a/src/app/messages/messages.component.spec.ts +++ b/src/app/messages/messages.component.spec.ts @@ -1,4 +1,4 @@ -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { ComponentFixture, ComponentFixtureAutoDetect, TestBed } from '@angular/core/testing'; import { MessagesComponent } from './messages.component'; import { MessageService } from '../message.service'; @@ -7,25 +7,80 @@ describe('MessagesComponent', () => { let component: MessagesComponent; let fixture: ComponentFixture; - beforeEach(async(() => { - TestBed.configureTestingModule({ - declarations: [ - MessagesComponent - ], - providers: [ - MessageService - ] - }) - .compileComponents(); + /** + * Configure `TestBed` + * @param messageServiceStub Message service or mock + * @return Promise + */ + function configureTestingModule(messageServiceStub = { messages: [] }) { + return TestBed + .configureTestingModule({ + declarations: [MessagesComponent], + providers: [ + { provide: ComponentFixtureAutoDetect, useValue: true }, + { provide: MessageService, useValue: messageServiceStub }, + ] + }) + .compileComponents() + .then(() => { + fixture = TestBed.createComponent(MessagesComponent); + component = fixture.componentInstance; + }); + } + + it('should create', () => configureTestingModule().then(() => { + expect(component).toBeTruthy(); })); - beforeEach(() => { - fixture = TestBed.createComponent(MessagesComponent); - component = fixture.componentInstance; - fixture.detectChanges(); + describe('when message service contains messages', () => { + const messages = ['foo', 'bar']; + beforeEach(() => configureTestingModule({ messages })); + + it('should display a "Messages" title', () => { + const titleElement = fixture.nativeElement.querySelector('h2'); + expect(titleElement).toBeDefined(); + expect(titleElement.textContent).toBe('Messages'); + }); + + it('should display a "clear" button', () => { + const clearButtonElement = fixture.nativeElement.querySelector('button.clear'); + expect(clearButtonElement).toBeDefined(); + expect(clearButtonElement.textContent).toBe('clear'); + }); + + it('should display messages from the message service', () => { + const messageElements = fixture.nativeElement.querySelector('div').querySelectorAll('div'); + expect(messageElements.length).toBe(2); + + const displayedMessages = Array.from(messageElements).map((d: HTMLElement) => d.textContent); + expect(displayedMessages).toEqual(messages); + }); + + describe('#clear button', () => { + it('should empty the message list when clicked', () => { + component.messageService.clear = jasmine.createSpy('clear'); + + const clearButtonElement = fixture.nativeElement.querySelector('button.clear'); + clearButtonElement.click(); + expect(component.messageService.clear).toHaveBeenCalled(); + }); + }); }); - it('should create', () => { - expect(component).toBeTruthy(); + describe('when message service contains no message', () => { + const messages = []; + beforeEach(() => configureTestingModule({ messages: [] })); + + it('should not display "Messages"', () => { + expect(fixture.nativeElement).not.toContain('Messages'); + }); + + it('should not display any button', () => { + expect(fixture.nativeElement.querySelectorAll('button').length).toBe(0); + }); + + it('should not display the container element', () => { + expect(fixture.nativeElement.querySelector('div')).toBe(null); + }); }); }); diff --git a/src/testing/async-observable-helpers.ts b/src/testing/async-observable-helpers.ts new file mode 100644 index 0000000..e64e99f --- /dev/null +++ b/src/testing/async-observable-helpers.ts @@ -0,0 +1,27 @@ +/* + * Mock async observables that return asynchronously. + * The observable either emits once and completes or errors. + * + * Must call `tick()` when test with `fakeAsync()`. + * + * THE FOLLOWING DON'T WORK + * Using `of().delay()` triggers TestBed errors; + * see https://github.com/angular/angular/issues/10127 . + * + * Using `asap` scheduler - as in `of(value, asap)` - doesn't work either. + */ +import { defer } from 'rxjs'; + +/** + * Create async observable that emits-once and completes after a JS engine turn + */ +export function asyncData(data: T) { + return defer(() => Promise.resolve(data)); +} + +/** + * Create async observable error that errors after a JS engine turn + */ +export function asyncError(errorObject: any) { + return defer(() => Promise.reject(errorObject)); +}