├── .editorconfig ├── .gitignore ├── .vscode ├── extensions.json ├── launch.json └── tasks.json ├── README.md ├── angular.json ├── e2e ├── app.e2e-spec.ts ├── app.po.ts └── tsconfig.e2e.json ├── package-lock.json ├── package.json ├── src ├── app │ ├── app-routing.module.ts │ ├── app.component.css │ ├── app.component.html │ ├── app.component.ts │ ├── app.module.ts │ ├── dashboard │ │ ├── dashboard.component.css │ │ ├── dashboard.component.html │ │ └── dashboard.component.ts │ ├── hero-detail │ │ ├── hero-detail.component.css │ │ ├── hero-detail.component.html │ │ └── hero-detail.component.ts │ ├── hero-search │ │ ├── hero-search.component.css │ │ ├── hero-search.component.html │ │ └── hero-search.component.ts │ ├── hero.service.ts │ ├── hero.ts │ ├── hero │ │ ├── hero.component.css │ │ ├── hero.component.html │ │ └── hero.component.ts │ ├── heroes │ │ ├── heroes.component.css │ │ ├── heroes.component.html │ │ └── heroes.component.ts │ ├── in-memory-data.service.ts │ ├── message.service.ts │ ├── messages │ │ ├── messages.component.css │ │ ├── messages.component.html │ │ └── messages.component.ts │ └── strength │ │ └── strength.pipe.ts ├── environments │ ├── environment.prod.ts │ └── environment.ts ├── favicon.ico ├── index.html ├── main.ts └── styles.css ├── tsconfig.app.json ├── tsconfig.json └── tsconfig.spec.json /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see https://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 | [*.ts] 12 | quote_type = single 13 | 14 | [*.md] 15 | max_line_length = off 16 | trim_trailing_whitespace = false 17 | -------------------------------------------------------------------------------- /.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 | /bazel-out 8 | 9 | # Node 10 | /node_modules 11 | npm-debug.log 12 | yarn-error.log 13 | 14 | # IDEs and editors 15 | .idea/ 16 | .project 17 | .classpath 18 | .c9/ 19 | *.launch 20 | .settings/ 21 | *.sublime-workspace 22 | 23 | # Visual Studio Code 24 | .vscode/* 25 | !.vscode/settings.json 26 | !.vscode/tasks.json 27 | !.vscode/launch.json 28 | !.vscode/extensions.json 29 | .history/* 30 | 31 | # Miscellaneous 32 | /.angular/cache 33 | .sass-cache/ 34 | /connect.lock 35 | /coverage 36 | /libpeerconnection.log 37 | testem.log 38 | /typings 39 | 40 | # System files 41 | .DS_Store 42 | Thumbs.db 43 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=827846 3 | "recommendations": ["angular.ng-template"] 4 | } 5 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 3 | "version": "0.2.0", 4 | "configurations": [ 5 | { 6 | "name": "ng serve", 7 | "type": "chrome", 8 | "request": "launch", 9 | "preLaunchTask": "npm: start", 10 | "url": "http://localhost:4200/" 11 | }, 12 | { 13 | "name": "ng test", 14 | "type": "chrome", 15 | "request": "launch", 16 | "preLaunchTask": "npm: test", 17 | "url": "http://localhost:9876/debug.html" 18 | } 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // For more information, visit: https://go.microsoft.com/fwlink/?LinkId=733558 3 | "version": "2.0.0", 4 | "tasks": [ 5 | { 6 | "type": "npm", 7 | "script": "start", 8 | "isBackground": true, 9 | "problemMatcher": { 10 | "owner": "typescript", 11 | "pattern": "$tsc", 12 | "background": { 13 | "activeOnStart": true, 14 | "beginsPattern": { 15 | "regexp": "(.*?)" 16 | }, 17 | "endsPattern": { 18 | "regexp": "bundle generation complete" 19 | } 20 | } 21 | } 22 | }, 23 | { 24 | "type": "npm", 25 | "script": "test", 26 | "isBackground": true, 27 | "problemMatcher": { 28 | "owner": "typescript", 29 | "pattern": "$tsc", 30 | "background": { 31 | "activeOnStart": true, 32 | "beginsPattern": { 33 | "regexp": "(.*?)" 34 | }, 35 | "endsPattern": { 36 | "regexp": "bundle generation complete" 37 | } 38 | } 39 | } 40 | } 41 | ] 42 | } 43 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ======= 2 | # Pluralsight's Unit Testing in Angular Course 3 | 4 | This course is up to date. 5 | 6 | To get started, clone the repo or download it from Github 7 | 8 | ## Development server 9 | 10 | Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The application will automatically reload if you change any of the source files. 11 | 12 | ## Code scaffolding 13 | 14 | Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`. 15 | 16 | ## Build 17 | 18 | Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. 19 | 20 | ## Running unit tests 21 | 22 | Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io). 23 | 24 | ## Running end-to-end tests 25 | 26 | Run `ng e2e` to execute the end-to-end tests via a platform of your choice. To use this command, you need to first add a package that implements end-to-end testing capabilities. 27 | 28 | ## Further help 29 | 30 | To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI Overview and Command Reference](https://angular.io/cli) page. 31 | -------------------------------------------------------------------------------- /angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "version": 1, 4 | "newProjectRoot": "projects", 5 | "projects": { 6 | "fresh17": { 7 | "projectType": "application", 8 | "schematics": {}, 9 | "root": "", 10 | "sourceRoot": "src", 11 | "prefix": "app", 12 | "architect": { 13 | "build": { 14 | "builder": "@angular-devkit/build-angular:application", 15 | "options": { 16 | "outputPath": "dist/fresh17", 17 | "index": "src/index.html", 18 | "browser": "src/main.ts", 19 | "polyfills": [ 20 | "zone.js" 21 | ], 22 | "tsConfig": "tsconfig.app.json", 23 | "assets": [ 24 | "src/favicon.ico", 25 | "src/assets" 26 | ], 27 | "styles": [ 28 | "src/styles.css" 29 | ], 30 | "scripts": [] 31 | }, 32 | "configurations": { 33 | "production": { 34 | "budgets": [ 35 | { 36 | "type": "initial", 37 | "maximumWarning": "500kb", 38 | "maximumError": "1mb" 39 | }, 40 | { 41 | "type": "anyComponentStyle", 42 | "maximumWarning": "2kb", 43 | "maximumError": "4kb" 44 | } 45 | ], 46 | "outputHashing": "all" 47 | }, 48 | "development": { 49 | "optimization": false, 50 | "extractLicenses": false, 51 | "sourceMap": true 52 | } 53 | }, 54 | "defaultConfiguration": "production" 55 | }, 56 | "serve": { 57 | "builder": "@angular-devkit/build-angular:dev-server", 58 | "configurations": { 59 | "production": { 60 | "buildTarget": "fresh17:build:production" 61 | }, 62 | "development": { 63 | "buildTarget": "fresh17:build:development" 64 | } 65 | }, 66 | "defaultConfiguration": "development" 67 | }, 68 | "extract-i18n": { 69 | "builder": "@angular-devkit/build-angular:extract-i18n", 70 | "options": { 71 | "buildTarget": "fresh17:build" 72 | } 73 | }, 74 | "test": { 75 | "builder": "@angular-devkit/build-angular:karma", 76 | "options": { 77 | "polyfills": [ 78 | "zone.js", 79 | "zone.js/testing" 80 | ], 81 | "tsConfig": "tsconfig.spec.json", 82 | "assets": [ 83 | "src/favicon.ico", 84 | "src/assets" 85 | ], 86 | "styles": [ 87 | "src/styles.css" 88 | ], 89 | "scripts": [] 90 | } 91 | } 92 | } 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /e2e/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; // necessary for es6 output in node 2 | 3 | import { browser, element, by, ElementFinder, ElementArrayFinder } from 'protractor'; 4 | import { promise } from 'selenium-webdriver'; 5 | 6 | const expectedH1 = 'Tour of Heroes'; 7 | const expectedTitle = `${expectedH1}`; 8 | const targetHero = { id: 15, name: 'Magneta' }; 9 | const targetHeroDashboardIndex = 3; 10 | const nameSuffix = 'X'; 11 | const newHeroName = targetHero.name + nameSuffix; 12 | 13 | class Hero { 14 | id: number; 15 | name: string; 16 | 17 | // Factory methods 18 | 19 | // Hero from string formatted as ' '. 20 | static fromString(s: string): Hero { 21 | return { 22 | id: +s.substr(0, s.indexOf(' ')), 23 | name: s.substr(s.indexOf(' ') + 1), 24 | }; 25 | } 26 | 27 | // Hero from hero list
  • element. 28 | static async fromLi(li: ElementFinder): Promise { 29 | let stringsFromA = await li.all(by.css('a')).getText(); 30 | let strings = stringsFromA[0].split(' '); 31 | return { id: +strings[0], name: strings[1] }; 32 | } 33 | 34 | // Hero id and name from the given detail element. 35 | static async fromDetail(detail: ElementFinder): Promise { 36 | // Get hero id from the first
    37 | let _id = await detail.all(by.css('div')).first().getText(); 38 | // Get name from the h2 39 | let _name = await detail.element(by.css('h2')).getText(); 40 | return { 41 | id: +_id.substr(_id.indexOf(' ') + 1), 42 | name: _name.substr(0, _name.lastIndexOf(' ')) 43 | }; 44 | } 45 | } 46 | 47 | describe('Tutorial part 6', () => { 48 | 49 | beforeAll(() => browser.get('')); 50 | 51 | function getPageElts() { 52 | let navElts = element.all(by.css('app-root nav a')); 53 | 54 | return { 55 | navElts: navElts, 56 | 57 | appDashboardHref: navElts.get(0), 58 | appDashboard: element(by.css('app-root app-dashboard')), 59 | topHeroes: element.all(by.css('app-root app-dashboard > div h4')), 60 | 61 | appHeroesHref: navElts.get(1), 62 | appHeroes: element(by.css('app-root app-heroes')), 63 | allHeroes: element.all(by.css('app-root app-heroes li')), 64 | selectedHeroSubview: element(by.css('app-root app-heroes > div:last-child')), 65 | 66 | heroDetail: element(by.css('app-root app-hero-detail > div')), 67 | 68 | searchBox: element(by.css('#search-box')), 69 | searchResults: element.all(by.css('.search-result li')) 70 | }; 71 | } 72 | 73 | describe('Initial page', () => { 74 | 75 | it(`has title '${expectedTitle}'`, () => { 76 | expect(browser.getTitle()).toEqual(expectedTitle); 77 | }); 78 | 79 | it(`has h1 '${expectedH1}'`, () => { 80 | expectHeading(1, expectedH1); 81 | }); 82 | 83 | const expectedViewNames = ['Dashboard', 'Heroes']; 84 | it(`has views ${expectedViewNames}`, () => { 85 | let viewNames = getPageElts().navElts.map((el: ElementFinder) => el.getText()); 86 | expect(viewNames).toEqual(expectedViewNames); 87 | }); 88 | 89 | it('has dashboard as the active view', () => { 90 | let page = getPageElts(); 91 | expect(page.appDashboard.isPresent()).toBeTruthy(); 92 | }); 93 | 94 | }); 95 | 96 | describe('Dashboard tests', () => { 97 | 98 | beforeAll(() => browser.get('')); 99 | 100 | it('has top heroes', () => { 101 | let page = getPageElts(); 102 | expect(page.topHeroes.count()).toEqual(4); 103 | }); 104 | 105 | it(`selects and routes to ${targetHero.name} details`, dashboardSelectTargetHero); 106 | 107 | it(`updates hero name (${newHeroName}) in details view`, updateHeroNameInDetailView); 108 | 109 | it(`cancels and shows ${targetHero.name} in Dashboard`, () => { 110 | element(by.buttonText('go back')).click(); 111 | browser.waitForAngular(); // seems necessary to gets tests to pass for toh-pt6 112 | 113 | let targetHeroElt = getPageElts().topHeroes.get(targetHeroDashboardIndex); 114 | expect(targetHeroElt.getText()).toEqual(targetHero.name); 115 | }); 116 | 117 | it(`selects and routes to ${targetHero.name} details`, dashboardSelectTargetHero); 118 | 119 | it(`updates hero name (${newHeroName}) in details view`, updateHeroNameInDetailView); 120 | 121 | it(`saves and shows ${newHeroName} in Dashboard`, () => { 122 | element(by.buttonText('save')).click(); 123 | browser.waitForAngular(); // seems necessary to gets tests to pass for toh-pt6 124 | 125 | let targetHeroElt = getPageElts().topHeroes.get(targetHeroDashboardIndex); 126 | expect(targetHeroElt.getText()).toEqual(newHeroName); 127 | }); 128 | 129 | }); 130 | 131 | describe('Heroes tests', () => { 132 | 133 | beforeAll(() => browser.get('')); 134 | 135 | it('can switch to Heroes view', () => { 136 | getPageElts().appHeroesHref.click(); 137 | let page = getPageElts(); 138 | expect(page.appHeroes.isPresent()).toBeTruthy(); 139 | expect(page.allHeroes.count()).toEqual(10, 'number of heroes'); 140 | }); 141 | 142 | it('can route to hero details', async () => { 143 | getHeroLiEltById(targetHero.id).click(); 144 | 145 | let page = getPageElts(); 146 | expect(page.heroDetail.isPresent()).toBeTruthy('shows hero detail'); 147 | let hero = await Hero.fromDetail(page.heroDetail); 148 | expect(hero.id).toEqual(targetHero.id); 149 | expect(hero.name).toEqual(targetHero.name.toUpperCase()); 150 | }); 151 | 152 | it(`updates hero name (${newHeroName}) in details view`, updateHeroNameInDetailView); 153 | 154 | it(`shows ${newHeroName} in Heroes list`, () => { 155 | element(by.buttonText('save')).click(); 156 | browser.waitForAngular(); 157 | let expectedText = `${targetHero.id} ${newHeroName}`; 158 | expect(getHeroAEltById(targetHero.id).getText()).toEqual(expectedText); 159 | }); 160 | 161 | it(`deletes ${newHeroName} from Heroes list`, async () => { 162 | const heroesBefore = await toHeroArray(getPageElts().allHeroes); 163 | const li = getHeroLiEltById(targetHero.id); 164 | li.element(by.buttonText('x')).click(); 165 | 166 | const page = getPageElts(); 167 | expect(page.appHeroes.isPresent()).toBeTruthy(); 168 | expect(page.allHeroes.count()).toEqual(9, 'number of heroes'); 169 | const heroesAfter = await toHeroArray(page.allHeroes); 170 | // console.log(await Hero.fromLi(page.allHeroes[0])); 171 | const expectedHeroes = heroesBefore.filter(h => h.name !== newHeroName); 172 | expect(heroesAfter).toEqual(expectedHeroes); 173 | // expect(page.selectedHeroSubview.isPresent()).toBeFalsy(); 174 | }); 175 | 176 | it(`adds back ${targetHero.name}`, async () => { 177 | const newHeroName = 'Alice'; 178 | const heroesBefore = await toHeroArray(getPageElts().allHeroes); 179 | const numHeroes = heroesBefore.length; 180 | 181 | element(by.css('input')).sendKeys(newHeroName); 182 | element(by.buttonText('add')).click(); 183 | 184 | let page = getPageElts(); 185 | let heroesAfter = await toHeroArray(page.allHeroes); 186 | expect(heroesAfter.length).toEqual(numHeroes + 1, 'number of heroes'); 187 | 188 | expect(heroesAfter.slice(0, numHeroes)).toEqual(heroesBefore, 'Old heroes are still there'); 189 | 190 | const maxId = heroesBefore[heroesBefore.length - 1].id; 191 | expect(heroesAfter[numHeroes]).toEqual({id: maxId + 1, name: newHeroName}); 192 | }); 193 | 194 | it('displays correctly styled buttons', async () => { 195 | element.all(by.buttonText('x')).then(buttons => { 196 | for (const button of buttons) { 197 | // Inherited styles from styles.css 198 | expect(button.getCssValue('font-family')).toBe('Arial'); 199 | expect(button.getCssValue('border')).toContain('none'); 200 | expect(button.getCssValue('padding')).toBe('5px 10px'); 201 | expect(button.getCssValue('border-radius')).toBe('4px'); 202 | // Styles defined in heroes.component.css 203 | expect(button.getCssValue('left')).toBe('194px'); 204 | expect(button.getCssValue('top')).toBe('-32px'); 205 | } 206 | }); 207 | 208 | const addButton = element(by.buttonText('add')); 209 | // Inherited styles from styles.css 210 | expect(addButton.getCssValue('font-family')).toBe('Arial'); 211 | expect(addButton.getCssValue('border')).toContain('none'); 212 | expect(addButton.getCssValue('padding')).toBe('5px 10px'); 213 | expect(addButton.getCssValue('border-radius')).toBe('4px'); 214 | }); 215 | 216 | }); 217 | 218 | describe('Progressive hero search', () => { 219 | 220 | beforeAll(() => browser.get('')); 221 | 222 | it(`searches for 'Ma'`, async () => { 223 | getPageElts().searchBox.sendKeys('Ma'); 224 | browser.sleep(1000); 225 | 226 | expect(getPageElts().searchResults.count()).toBe(4); 227 | }); 228 | 229 | it(`continues search with 'g'`, async () => { 230 | getPageElts().searchBox.sendKeys('g'); 231 | browser.sleep(1000); 232 | expect(getPageElts().searchResults.count()).toBe(2); 233 | }); 234 | 235 | it(`continues search with 'e' and gets ${targetHero.name}`, async () => { 236 | getPageElts().searchBox.sendKeys('n'); 237 | browser.sleep(1000); 238 | let page = getPageElts(); 239 | expect(page.searchResults.count()).toBe(1); 240 | let hero = page.searchResults.get(0); 241 | expect(hero.getText()).toEqual(targetHero.name); 242 | }); 243 | 244 | it(`navigates to ${targetHero.name} details view`, async () => { 245 | let hero = getPageElts().searchResults.get(0); 246 | expect(hero.getText()).toEqual(targetHero.name); 247 | hero.click(); 248 | 249 | let page = getPageElts(); 250 | expect(page.heroDetail.isPresent()).toBeTruthy('shows hero detail'); 251 | let hero2 = await Hero.fromDetail(page.heroDetail); 252 | expect(hero2.id).toEqual(targetHero.id); 253 | expect(hero2.name).toEqual(targetHero.name.toUpperCase()); 254 | }); 255 | }); 256 | 257 | async function dashboardSelectTargetHero() { 258 | let targetHeroElt = getPageElts().topHeroes.get(targetHeroDashboardIndex); 259 | expect(targetHeroElt.getText()).toEqual(targetHero.name); 260 | targetHeroElt.click(); 261 | browser.waitForAngular(); // seems necessary to gets tests to pass for toh-pt6 262 | 263 | let page = getPageElts(); 264 | expect(page.heroDetail.isPresent()).toBeTruthy('shows hero detail'); 265 | let hero = await Hero.fromDetail(page.heroDetail); 266 | expect(hero.id).toEqual(targetHero.id); 267 | expect(hero.name).toEqual(targetHero.name.toUpperCase()); 268 | } 269 | 270 | async function updateHeroNameInDetailView() { 271 | // Assumes that the current view is the hero details view. 272 | addToHeroName(nameSuffix); 273 | 274 | let page = getPageElts(); 275 | let hero = await Hero.fromDetail(page.heroDetail); 276 | expect(hero.id).toEqual(targetHero.id); 277 | expect(hero.name).toEqual(newHeroName.toUpperCase()); 278 | } 279 | 280 | }); 281 | 282 | function addToHeroName(text: string): promise.Promise { 283 | let input = element(by.css('input')); 284 | return input.sendKeys(text); 285 | } 286 | 287 | function expectHeading(hLevel: number, expectedText: string): void { 288 | let hTag = `h${hLevel}`; 289 | let hText = element(by.css(hTag)).getText(); 290 | expect(hText).toEqual(expectedText, hTag); 291 | }; 292 | 293 | function getHeroAEltById(id: number): ElementFinder { 294 | let spanForId = element(by.cssContainingText('li span.badge', id.toString())); 295 | return spanForId.element(by.xpath('..')); 296 | } 297 | 298 | function getHeroLiEltById(id: number): ElementFinder { 299 | let spanForId = element(by.cssContainingText('li span.badge', id.toString())); 300 | return spanForId.element(by.xpath('../..')); 301 | } 302 | 303 | async function toHeroArray(allHeroes: ElementArrayFinder): Promise { 304 | let promisedHeroes = await allHeroes.map(Hero.fromLi); 305 | // The cast is necessary to get around issuing with the signature of Promise.all() 306 | return > Promise.all(promisedHeroes); 307 | } 308 | -------------------------------------------------------------------------------- /e2e/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/e2e", 5 | "baseUrl": "./", 6 | "module": "commonjs", 7 | "target": "es5", 8 | "types": [ 9 | "jasmine", 10 | "jasminewd2", 11 | "node" 12 | ] 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fresh17", 3 | "version": "0.0.0", 4 | "scripts": { 5 | "ng": "ng", 6 | "start": "ng serve", 7 | "build": "ng build", 8 | "watch": "ng build --watch --configuration development", 9 | "test": "ng test" 10 | }, 11 | "private": true, 12 | "dependencies": { 13 | "@angular/animations": "^17.1.0", 14 | "@angular/common": "^17.1.0", 15 | "@angular/compiler": "^17.1.0", 16 | "@angular/core": "^17.1.0", 17 | "@angular/forms": "^17.1.0", 18 | "@angular/platform-browser": "^17.1.0", 19 | "@angular/platform-browser-dynamic": "^17.1.0", 20 | "@angular/router": "^17.1.0", 21 | "rxjs": "~7.8.0", 22 | "tslib": "^2.3.0", 23 | "angular-in-memory-web-api": "^0.17.0", 24 | "zone.js": "~0.14.3" 25 | }, 26 | "devDependencies": { 27 | "@angular-devkit/build-angular": "^17.1.0", 28 | "@angular/cli": "^17.1.0", 29 | "@angular/compiler-cli": "^17.1.0", 30 | "@types/jasmine": "~5.1.0", 31 | "jasmine-core": "~5.1.0", 32 | "karma": "~6.4.0", 33 | "karma-chrome-launcher": "~3.2.0", 34 | "karma-coverage": "~2.2.0", 35 | "karma-jasmine": "~5.1.0", 36 | "karma-jasmine-html-reporter": "~2.1.0", 37 | "typescript": "~5.3.2" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/app/app-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { RouterModule, Routes } from '@angular/router'; 3 | 4 | import { DashboardComponent } from './dashboard/dashboard.component'; 5 | import { HeroesComponent } from './heroes/heroes.component'; 6 | import { HeroDetailComponent } from './hero-detail/hero-detail.component'; 7 | 8 | const routes: Routes = [ 9 | { path: '', redirectTo: '/dashboard', pathMatch: 'full' }, 10 | { path: 'dashboard', component: DashboardComponent }, 11 | { path: 'detail/:id', component: HeroDetailComponent }, 12 | { path: 'heroes', component: HeroesComponent } 13 | ]; 14 | 15 | @NgModule({ 16 | imports: [ RouterModule.forRoot(routes) ], 17 | exports: [ RouterModule ] 18 | }) 19 | export class AppRoutingModule {} 20 | -------------------------------------------------------------------------------- /src/app/app.component.css: -------------------------------------------------------------------------------- 1 | /* AppComponent's private CSS styles */ 2 | h1 { 3 | font-size: 1.2em; 4 | color: #999; 5 | margin-bottom: 0; 6 | } 7 | h2 { 8 | font-size: 2em; 9 | margin-top: 0; 10 | padding-top: 0; 11 | } 12 | nav a { 13 | padding: 5px 10px; 14 | text-decoration: none; 15 | margin-top: 10px; 16 | display: inline-block; 17 | background-color: #eee; 18 | border-radius: 4px; 19 | } 20 | nav a:visited, a:link { 21 | color: #607D8B; 22 | } 23 | nav a:hover { 24 | color: #039be5; 25 | background-color: #CFD8DC; 26 | } 27 | nav a.active { 28 | color: #039be5; 29 | } 30 | -------------------------------------------------------------------------------- /src/app/app.component.html: -------------------------------------------------------------------------------- 1 |

    {{title}}

    2 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /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.css'] 7 | }) 8 | export class AppComponent { 9 | title = 'Tour of Heroes'; 10 | } 11 | -------------------------------------------------------------------------------- /src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { BrowserModule } from '@angular/platform-browser'; 3 | import { FormsModule } from '@angular/forms'; 4 | import { HttpClientModule } from '@angular/common/http'; 5 | 6 | import { HttpClientInMemoryWebApiModule } from 'angular-in-memory-web-api'; 7 | import { InMemoryDataService } from './in-memory-data.service'; 8 | 9 | import { AppRoutingModule } from './app-routing.module'; 10 | 11 | import { AppComponent } from './app.component'; 12 | import { DashboardComponent } from './dashboard/dashboard.component'; 13 | import { HeroDetailComponent } from './hero-detail/hero-detail.component'; 14 | import { HeroesComponent } from './heroes/heroes.component'; 15 | import { HeroSearchComponent } from './hero-search/hero-search.component'; 16 | import { HeroService } from './hero.service'; 17 | import { MessageService } from './message.service'; 18 | import { MessagesComponent } from './messages/messages.component'; 19 | import { StrengthPipe } from './strength/strength.pipe'; 20 | import { HeroComponent } from './hero/hero.component'; 21 | 22 | @NgModule({ 23 | imports: [ 24 | BrowserModule, 25 | FormsModule, 26 | AppRoutingModule, 27 | HttpClientModule, 28 | 29 | // The HttpClientInMemoryWebApiModule module intercepts HTTP requests 30 | // and returns simulated server responses. 31 | // Remove it when a real server is ready to receive requests. 32 | HttpClientInMemoryWebApiModule.forRoot( 33 | InMemoryDataService, { dataEncapsulation: false } 34 | ) 35 | ], 36 | declarations: [ 37 | AppComponent, 38 | DashboardComponent, 39 | HeroesComponent, 40 | HeroDetailComponent, 41 | MessagesComponent, 42 | HeroSearchComponent, 43 | StrengthPipe, 44 | HeroComponent 45 | ], 46 | providers: [ HeroService, MessageService ], 47 | bootstrap: [ AppComponent ] 48 | }) 49 | export class AppModule { } 50 | -------------------------------------------------------------------------------- /src/app/dashboard/dashboard.component.css: -------------------------------------------------------------------------------- 1 | /* DashboardComponent's private CSS styles */ 2 | [class*='col-'] { 3 | float: left; 4 | padding-right: 20px; 5 | padding-bottom: 20px; 6 | } 7 | [class*='col-']:last-of-type { 8 | padding-right: 0; 9 | } 10 | a { 11 | text-decoration: none; 12 | } 13 | *, *:after, *:before { 14 | -webkit-box-sizing: border-box; 15 | -moz-box-sizing: border-box; 16 | box-sizing: border-box; 17 | } 18 | h3 { 19 | text-align: center; margin-bottom: 0; 20 | } 21 | h4 { 22 | position: relative; 23 | } 24 | .grid { 25 | margin: 0; 26 | } 27 | .col-1-4 { 28 | width: 25%; 29 | } 30 | .module { 31 | padding: 20px; 32 | text-align: center; 33 | color: #eee; 34 | max-height: 120px; 35 | min-width: 120px; 36 | background-color: #607D8B; 37 | border-radius: 2px; 38 | } 39 | .module:hover { 40 | background-color: #EEE; 41 | cursor: pointer; 42 | color: #607d8b; 43 | } 44 | .grid-pad { 45 | padding: 10px 0; 46 | } 47 | .grid-pad > [class*='col-']:last-of-type { 48 | padding-right: 20px; 49 | } 50 | @media (max-width: 600px) { 51 | .module { 52 | font-size: 10px; 53 | max-height: 75px; } 54 | } 55 | @media (max-width: 1024px) { 56 | .grid { 57 | margin: 0; 58 | } 59 | .module { 60 | min-width: 60px; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/app/dashboard/dashboard.component.html: -------------------------------------------------------------------------------- 1 |

    Top Heroes

    2 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/app/dashboard/dashboard.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { Hero } from '../hero'; 3 | import { HeroService } from '../hero.service'; 4 | 5 | @Component({ 6 | selector: 'app-dashboard', 7 | templateUrl: './dashboard.component.html', 8 | styleUrls: [ './dashboard.component.css' ] 9 | }) 10 | export class DashboardComponent implements OnInit { 11 | heroes: Hero[] = []; 12 | 13 | constructor(private heroService: HeroService) { } 14 | 15 | ngOnInit() { 16 | this.getHeroes(); 17 | } 18 | 19 | getHeroes(): void { 20 | this.heroService.getHeroes() 21 | .subscribe(heroes => this.heroes = heroes.slice(1, 5)); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/app/hero-detail/hero-detail.component.css: -------------------------------------------------------------------------------- 1 | /* HeroDetailComponent's private CSS styles */ 2 | label { 3 | display: inline-block; 4 | width: 3em; 5 | margin: .5em 0; 6 | color: #607D8B; 7 | font-weight: bold; 8 | } 9 | input { 10 | height: 2em; 11 | font-size: 1em; 12 | padding-left: .4em; 13 | } 14 | button { 15 | margin-top: 20px; 16 | font-family: Arial; 17 | background-color: #eee; 18 | border: none; 19 | padding: 5px 10px; 20 | border-radius: 4px; 21 | cursor: pointer; cursor: hand; 22 | } 23 | button:hover { 24 | background-color: #cfd8dc; 25 | } 26 | button:disabled { 27 | background-color: #eee; 28 | color: #ccc; 29 | cursor: auto; 30 | } 31 | -------------------------------------------------------------------------------- /src/app/hero-detail/hero-detail.component.html: -------------------------------------------------------------------------------- 1 |
    2 |

    {{ hero.name | uppercase }} Details

    3 |
    id: {{hero.id}}
    4 |
    5 | 8 |
    9 | 10 | 11 |
    12 | -------------------------------------------------------------------------------- /src/app/hero-detail/hero-detail.component.ts: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | import { Component, OnInit, Input } from '@angular/core'; 3 | import { ActivatedRoute } from '@angular/router'; 4 | import { Location } from '@angular/common'; 5 | 6 | import { Hero } from '../hero'; 7 | import { HeroService } from '../hero.service'; 8 | 9 | @Component({ 10 | selector: 'app-hero-detail', 11 | templateUrl: './hero-detail.component.html', 12 | styleUrls: ['./hero-detail.component.css'], 13 | }) 14 | export class HeroDetailComponent implements OnInit { 15 | @Input() hero: Hero; 16 | 17 | constructor( 18 | private route: ActivatedRoute, 19 | private heroService: HeroService, 20 | private location: Location 21 | ) {} 22 | 23 | ngOnInit(): void { 24 | this.getHero(); 25 | } 26 | 27 | getHero(): void { 28 | const id = +this.route.snapshot.paramMap.get('id'); 29 | this.heroService.getHero(id).subscribe((hero) => (this.hero = hero)); 30 | } 31 | 32 | goBack(): void { 33 | this.location.back(); 34 | } 35 | 36 | save(): void { 37 | var p = new Promise((resolve) => { 38 | this.heroService.updateHero(this.hero).subscribe(() => this.goBack()); 39 | resolve(); 40 | }); 41 | } 42 | 43 | // save(): void { 44 | // debounce(() => { 45 | // this.heroService.updateHero(this.hero) 46 | // .subscribe(() => this.goBack()); 47 | // }, 250, false)(); 48 | // } 49 | } 50 | 51 | // function debounce(func, wait, immediate) { 52 | // var timeout; 53 | // return function () { 54 | // var context = this, 55 | // args = arguments; 56 | // var later = function () { 57 | // timeout = null; 58 | // if (!immediate) func.apply(context, args); 59 | // }; 60 | // var callNow = immediate && !timeout; 61 | // clearTimeout(timeout); 62 | // timeout = setTimeout(later, wait); 63 | // if (callNow) func.apply(context, args); 64 | // }; 65 | // } 66 | -------------------------------------------------------------------------------- /src/app/hero-search/hero-search.component.css: -------------------------------------------------------------------------------- 1 | /* HeroSearch private styles */ 2 | .search-result li { 3 | border-bottom: 1px solid gray; 4 | border-left: 1px solid gray; 5 | border-right: 1px solid gray; 6 | width:195px; 7 | height: 16px; 8 | padding: 5px; 9 | background-color: white; 10 | cursor: pointer; 11 | list-style-type: none; 12 | } 13 | 14 | .search-result li:hover { 15 | background-color: #607D8B; 16 | } 17 | 18 | .search-result li a { 19 | color: #888; 20 | display: block; 21 | text-decoration: none; 22 | } 23 | 24 | .search-result li a:hover { 25 | color: white; 26 | } 27 | .search-result li a:active { 28 | color: white; 29 | } 30 | #search-box { 31 | width: 200px; 32 | height: 20px; 33 | } 34 | 35 | 36 | ul.search-result { 37 | margin-top: 0; 38 | padding-left: 0; 39 | } 40 | -------------------------------------------------------------------------------- /src/app/hero-search/hero-search.component.html: -------------------------------------------------------------------------------- 1 |
    2 |

    Hero Search

    3 | 4 | 5 | 6 | 13 |
    14 | -------------------------------------------------------------------------------- /src/app/hero-search/hero-search.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | 3 | import { Observable, Subject } from 'rxjs'; 4 | 5 | import { debounceTime, distinctUntilChanged, switchMap } from 'rxjs/operators'; 6 | 7 | import { Hero } from '../hero'; 8 | import { HeroService } from '../hero.service'; 9 | 10 | @Component({ 11 | selector: 'app-hero-search', 12 | templateUrl: './hero-search.component.html', 13 | styleUrls: ['./hero-search.component.css'], 14 | }) 15 | export class HeroSearchComponent implements OnInit { 16 | heroes$: Observable; 17 | private searchTerms = new Subject(); 18 | 19 | constructor(private heroService: HeroService) {} 20 | 21 | // Push a search term into the observable stream. 22 | search(term: string): void { 23 | this.searchTerms.next(term); 24 | } 25 | 26 | ngOnInit(): void { 27 | this.heroes$ = this.searchTerms.pipe( 28 | // wait 300ms after each keystroke before considering the term 29 | debounceTime(300), 30 | 31 | // ignore new term if same as previous term 32 | distinctUntilChanged(), 33 | 34 | // switch to new search observable each time the term changes 35 | switchMap((term: string) => this.heroService.searchHeroes(term)) 36 | ); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/app/hero.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { HttpClient, HttpHeaders } from '@angular/common/http'; 3 | 4 | import { Observable, of } from 'rxjs'; 5 | import { catchError, map, tap } from 'rxjs/operators'; 6 | 7 | import { Hero } from './hero'; 8 | import { MessageService } from './message.service'; 9 | 10 | const httpOptions = { 11 | headers: new HttpHeaders({ 'Content-Type': 'application/json' }) 12 | }; 13 | 14 | @Injectable() 15 | export class HeroService { 16 | 17 | private heroesUrl = 'api/heroes'; // URL to web api 18 | 19 | constructor( 20 | private http: HttpClient, 21 | private messageService: MessageService) { } 22 | 23 | /** GET heroes from the server */ 24 | getHeroes (): Observable { 25 | return this.http.get(this.heroesUrl) 26 | .pipe( 27 | tap(heroes => this.log(`fetched heroes`)), 28 | catchError(this.handleError('getHeroes', [])) 29 | ); 30 | } 31 | 32 | /** GET hero by id. Return `undefined` when id not found */ 33 | getHeroNo404(id: number): Observable { 34 | const url = `${this.heroesUrl}/?id=${id}`; 35 | return this.http.get(url) 36 | .pipe( 37 | map(heroes => heroes[0]), // returns a {0|1} element array 38 | tap(h => { 39 | const outcome = h ? `fetched` : `did not find`; 40 | this.log(`${outcome} hero id=${id}`); 41 | }), 42 | catchError(this.handleError(`getHero id=${id}`)) 43 | ); 44 | } 45 | 46 | /** GET hero by id. Will 404 if id not found */ 47 | getHero(id: number): Observable { 48 | const url = `${this.heroesUrl}/${id}`; 49 | return this.http.get(url).pipe( 50 | tap(_ => this.log(`fetched hero id=${id}`)), 51 | catchError(this.handleError(`getHero id=${id}`)) 52 | ); 53 | } 54 | 55 | /* GET heroes whose name contains search term */ 56 | searchHeroes(term: string): Observable { 57 | if (!term.trim()) { 58 | // if not search term, return empty hero array. 59 | return of([]); 60 | } 61 | return this.http.get(`api/heroes/?name=${term}`).pipe( 62 | tap(_ => this.log(`found heroes matching "${term}"`)), 63 | catchError(this.handleError('searchHeroes', [])) 64 | ); 65 | } 66 | 67 | //////// Save methods ////////// 68 | 69 | /** POST: add a new hero to the server */ 70 | addHero (hero: Hero): Observable { 71 | return this.http.post(this.heroesUrl, hero, httpOptions).pipe( 72 | tap((hero: Hero) => this.log(`added hero w/ id=${hero.id}`)), 73 | catchError(this.handleError('addHero')) 74 | ); 75 | } 76 | 77 | /** DELETE: delete the hero from the server */ 78 | deleteHero (hero: Hero | number): Observable { 79 | const id = typeof hero === 'number' ? hero : hero.id; 80 | const url = `${this.heroesUrl}/${id}`; 81 | 82 | return this.http.delete(url, httpOptions).pipe( 83 | tap(_ => this.log(`deleted hero id=${id}`)), 84 | catchError(this.handleError('deleteHero')) 85 | ); 86 | } 87 | 88 | /** PUT: update the hero on the server */ 89 | updateHero (hero: Hero): Observable { 90 | return this.http.put(this.heroesUrl, hero, httpOptions).pipe( 91 | tap(_ => this.log(`updated hero id=${hero.id}`)), 92 | catchError(this.handleError('updateHero')) 93 | ); 94 | } 95 | 96 | /** 97 | * Handle Http operation that failed. 98 | * Let the app continue. 99 | * @param operation - name of the operation that failed 100 | * @param result - optional value to return as the observable result 101 | */ 102 | private handleError (operation = 'operation', result?: T) { 103 | return (error: any): Observable => { 104 | 105 | // TODO: send the error to remote logging infrastructure 106 | console.error(error); // log to console instead 107 | 108 | // TODO: better job of transforming error for user consumption 109 | this.log(`${operation} failed: ${error.message}`); 110 | 111 | // Let the app keep running by returning an empty result. 112 | return of(result as T); 113 | }; 114 | } 115 | 116 | /** Log a HeroService message with the MessageService */ 117 | private log(message: string) { 118 | this.messageService.add('HeroService: ' + message); 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /src/app/hero.ts: -------------------------------------------------------------------------------- 1 | export class Hero { 2 | id: number; 3 | name: string; 4 | strength: number; 5 | } 6 | -------------------------------------------------------------------------------- /src/app/hero/hero.component.css: -------------------------------------------------------------------------------- 1 | /* HeroComponent's private CSS styles */ 2 | 3 | a { 4 | color: #888; 5 | text-decoration: none; 6 | position: relative; 7 | display: block; 8 | width: 250px; 9 | } 10 | 11 | a:hover { 12 | color:#607D8B; 13 | } 14 | 15 | .badge { 16 | display: inline-block; 17 | font-size: small; 18 | color: white; 19 | padding: 0.8em 0.7em 0 0.7em; 20 | background-color: #607D8B; 21 | line-height: 1em; 22 | position: relative; 23 | left: -1px; 24 | top: -4px; 25 | height: 1.8em; 26 | min-width: 16px; 27 | text-align: right; 28 | margin-right: .8em; 29 | border-radius: 4px 0 0 4px; 30 | } 31 | 32 | button { 33 | background-color: #eee; 34 | border: none; 35 | padding: 5px 10px; 36 | border-radius: 4px; 37 | cursor: pointer; 38 | cursor: hand; 39 | font-family: Arial; 40 | } 41 | 42 | button:hover { 43 | background-color: #cfd8dc; 44 | } 45 | 46 | button.delete { 47 | position: relative; 48 | left: 194px; 49 | top: -32px; 50 | background-color: gray !important; 51 | color: white; 52 | } 53 | 54 | -------------------------------------------------------------------------------- /src/app/hero/hero.component.html: -------------------------------------------------------------------------------- 1 | 2 | {{hero.id}} {{hero.name}} 3 | 4 | -------------------------------------------------------------------------------- /src/app/hero/hero.component.ts: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | import { Component, Input, Output, EventEmitter } from "@angular/core"; 3 | import { Hero } from "../hero"; 4 | 5 | @Component({ 6 | selector: "app-hero", 7 | templateUrl: "./hero.component.html", 8 | styleUrls: ["./hero.component.css"], 9 | }) 10 | export class HeroComponent { 11 | @Input() hero: Hero; 12 | @Output() delete = new EventEmitter(); 13 | 14 | onDeleteClick($event): void { 15 | $event.stopPropagation(); 16 | this.delete.next(); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/app/heroes/heroes.component.css: -------------------------------------------------------------------------------- 1 | /* HeroesComponent's private CSS styles */ 2 | .heroes { 3 | margin: 0 0 2em 0; 4 | list-style-type: none; 5 | padding: 0; 6 | width: 15em; 7 | } 8 | .heroes li { 9 | position: relative; 10 | cursor: pointer; 11 | background-color: #EEE; 12 | margin: .5em; 13 | padding: .3em 0; 14 | height: 1.6em; 15 | border-radius: 4px; 16 | } 17 | 18 | .heroes li:hover { 19 | color: #607D8B; 20 | background-color: #DDD; 21 | left: .1em; 22 | } 23 | 24 | -------------------------------------------------------------------------------- /src/app/heroes/heroes.component.html: -------------------------------------------------------------------------------- 1 |

    My Heroes

    2 | 3 |
    4 | 7 | 8 | 11 |
    12 | 13 |
      14 |
    • 15 | 16 |
    • 17 |
    18 | -------------------------------------------------------------------------------- /src/app/heroes/heroes.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | 3 | import { Hero } from '../hero'; 4 | import { HeroService } from '../hero.service'; 5 | 6 | @Component({ 7 | selector: 'app-heroes', 8 | templateUrl: './heroes.component.html', 9 | styleUrls: ['./heroes.component.css'], 10 | }) 11 | export class HeroesComponent implements OnInit { 12 | heroes: Hero[]; 13 | 14 | constructor(private heroService: HeroService) {} 15 | 16 | ngOnInit() { 17 | this.getHeroes(); 18 | } 19 | 20 | getHeroes(): void { 21 | this.heroService.getHeroes().subscribe((heroes) => (this.heroes = heroes)); 22 | } 23 | 24 | add(name: string): void { 25 | name = name.trim(); 26 | var strength = 11; 27 | if (!name) { 28 | return; 29 | } 30 | this.heroService.addHero({ name, strength } as Hero).subscribe((hero) => { 31 | this.heroes.push(hero); 32 | }); 33 | } 34 | 35 | delete(hero: Hero): void { 36 | this.heroes = this.heroes.filter((h) => h !== hero); 37 | this.heroService.deleteHero(hero).subscribe(); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/app/in-memory-data.service.ts: -------------------------------------------------------------------------------- 1 | import { InMemoryDbService } from 'angular-in-memory-web-api'; 2 | 3 | export class InMemoryDataService implements InMemoryDbService { 4 | createDb() { 5 | const heroes = [ 6 | { id: 11, name: 'Mr. Nice', strength: 10 }, 7 | { id: 12, name: 'Narco', strength: 5 }, 8 | { id: 13, name: 'Bombasto', strength: 8 }, 9 | { id: 14, name: 'Celeritas', strength: 15 }, 10 | { id: 15, name: 'Magneta', strength: 22 }, 11 | { id: 16, name: 'RubberMan', strength: 50 }, 12 | { id: 17, name: 'Dynama', strength: 43 }, 13 | { id: 18, name: 'Dr IQ', strength: 4 }, 14 | { id: 19, name: 'Magma', strength: 18 }, 15 | { id: 20, name: 'Tornado', strength: 15 } 16 | ]; 17 | return {heroes}; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/app/message.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | 3 | @Injectable() 4 | export class MessageService { 5 | messages: string[] = []; 6 | 7 | add(message: string) { 8 | this.messages.push(message); 9 | } 10 | 11 | clear() { 12 | this.messages = []; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/app/messages/messages.component.css: -------------------------------------------------------------------------------- 1 | /* MessagesComponent's private CSS styles */ 2 | h2 { 3 | color: red; 4 | font-family: Arial, Helvetica, sans-serif; 5 | font-weight: lighter; 6 | } 7 | body { 8 | margin: 2em; 9 | } 10 | body, input[text], button { 11 | color: crimson; 12 | font-family: Cambria, Georgia; 13 | } 14 | 15 | button.clear { 16 | font-family: Arial; 17 | background-color: #eee; 18 | border: none; 19 | padding: 5px 10px; 20 | border-radius: 4px; 21 | cursor: pointer; 22 | cursor: hand; 23 | } 24 | button:hover { 25 | background-color: #cfd8dc; 26 | } 27 | button:disabled { 28 | background-color: #eee; 29 | color: #aaa; 30 | cursor: auto; 31 | } 32 | button.clear { 33 | color: #888; 34 | margin-bottom: 12px; 35 | } 36 | -------------------------------------------------------------------------------- /src/app/messages/messages.component.html: -------------------------------------------------------------------------------- 1 |
    2 | 3 |

    Messages

    4 | 6 |
    {{message}}
    7 | 8 |
    9 | -------------------------------------------------------------------------------- /src/app/messages/messages.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { MessageService } from '../message.service'; 3 | 4 | @Component({ 5 | selector: 'app-messages', 6 | templateUrl: './messages.component.html', 7 | styleUrls: ['./messages.component.css'] 8 | }) 9 | export class MessagesComponent implements OnInit { 10 | 11 | constructor(public messageService: MessageService) {} 12 | 13 | ngOnInit() { 14 | } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /src/app/strength/strength.pipe.ts: -------------------------------------------------------------------------------- 1 | import { Pipe, PipeTransform } from '@angular/core'; 2 | 3 | @Pipe({ 4 | name: 'strength' 5 | }) 6 | export class StrengthPipe implements PipeTransform { 7 | transform(value: number): string { 8 | if(value < 10) { 9 | return value + " (weak)"; 10 | } else if(value >= 10 && value < 20) { 11 | return value + " (strong)"; 12 | } else { 13 | return value + " (unbelievable)"; 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true 3 | }; 4 | -------------------------------------------------------------------------------- /src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // The file contents for the current environment will overwrite these during build. 2 | // The build system defaults to the dev environment which uses `environment.ts`, but if you do 3 | // `ng build --env=prod` then `environment.prod.ts` will be used instead. 4 | // The list of which env maps to which file can be found in `.angular-cli.json`. 5 | 6 | export const environment = { 7 | production: false 8 | }; 9 | -------------------------------------------------------------------------------- /src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joeeames/PSAngularUnitTestingCourse/988d60788c139a6d77f044bad15f20d427bb0ab9/src/favicon.ico -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Fresh17 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/styles.css: -------------------------------------------------------------------------------- 1 | /* Master Styles */ 2 | h1 { 3 | color: #369; 4 | font-family: Arial, Helvetica, sans-serif; 5 | font-size: 250%; 6 | } 7 | h2, h3 { 8 | color: #444; 9 | font-family: Arial, Helvetica, sans-serif; 10 | font-weight: lighter; 11 | } 12 | body { 13 | margin: 2em; 14 | } 15 | body, input[text], button { 16 | color: #888; 17 | font-family: Cambria, Georgia; 18 | } 19 | a { 20 | cursor: pointer; 21 | cursor: hand; 22 | } 23 | button { 24 | font-family: Arial; 25 | background-color: #eee; 26 | border: none; 27 | padding: 5px 10px; 28 | border-radius: 4px; 29 | cursor: pointer; 30 | cursor: hand; 31 | } 32 | button:hover { 33 | background-color: #cfd8dc; 34 | } 35 | button:disabled { 36 | background-color: #eee; 37 | color: #aaa; 38 | cursor: auto; 39 | } 40 | 41 | /* Navigation link styles */ 42 | nav a { 43 | padding: 5px 10px; 44 | text-decoration: none; 45 | margin-right: 10px; 46 | margin-top: 10px; 47 | display: inline-block; 48 | background-color: #eee; 49 | border-radius: 4px; 50 | } 51 | nav a:visited, a:link { 52 | color: #607D8B; 53 | } 54 | nav a:hover { 55 | color: #039be5; 56 | background-color: #CFD8DC; 57 | } 58 | nav a.active { 59 | color: #039be5; 60 | } 61 | 62 | /* everywhere else */ 63 | * { 64 | font-family: Arial, Helvetica, sans-serif; 65 | } 66 | -------------------------------------------------------------------------------- /tsconfig.app.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "./tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "./out-tsc/app", 6 | "types": [] 7 | }, 8 | "files": [ 9 | "src/main.ts" 10 | ], 11 | "include": [ 12 | "src/**/*.d.ts" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "compileOnSave": false, 4 | "compilerOptions": { 5 | "outDir": "./dist/out-tsc", 6 | "forceConsistentCasingInFileNames": true, 7 | "strict": true, 8 | "noImplicitOverride": true, 9 | "noPropertyAccessFromIndexSignature": true, 10 | "noImplicitReturns": true, 11 | "noImplicitAny": false, 12 | "strictPropertyInitialization": false, 13 | "noFallthroughCasesInSwitch": true, 14 | "skipLibCheck": true, 15 | "esModuleInterop": true, 16 | "sourceMap": true, 17 | "declaration": false, 18 | "experimentalDecorators": true, 19 | "moduleResolution": "node", 20 | "importHelpers": true, 21 | "target": "ES2022", 22 | "module": "ES2022", 23 | "useDefineForClassFields": false, 24 | "lib": ["ES2022", "dom"] 25 | }, 26 | "angularCompilerOptions": { 27 | "enableI18nLegacyMessageIdFormat": false, 28 | "strictInjectionParameters": true, 29 | "strictInputAccessModifiers": true, 30 | "strictTemplates": true 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "./tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "./out-tsc/spec", 6 | "types": [ 7 | "jasmine" 8 | ] 9 | }, 10 | "include": [ 11 | "src/**/*.spec.ts", 12 | "src/**/*.d.ts" 13 | ] 14 | } 15 | --------------------------------------------------------------------------------