feat(challenge17): router testing

This commit is contained in:
thomas
2023-03-09 16:47:07 +01:00
parent 5bfe74ff23
commit c546390fae
25 changed files with 778 additions and 345 deletions

View File

@@ -54,6 +54,11 @@ This goal of this project is to help you get better at Angular and NgRx by resol
<a href="./apps/ngrx-1/README.md"><img src="https://img.shields.io/badge/2-Effect vs Selector-orange" alt="Effect vs Selector"/></a>
<a href="./apps/ngrx-notification/README.md"><img src="https://img.shields.io/badge/7-Power of Effects-red" alt="power of Effects"/></a>
</br>
<img src="https://img.shields.io/badge/Testing--gray" alt="testing"/>
<a href="./apps/router-testing/README.md"><img src="https://img.shields.io/badge/17-Router Testing-orange" alt="router Testing"/></a>
## Contributors ✨
<!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->
@@ -62,7 +67,7 @@ This goal of this project is to help you get better at Angular and NgRx by resol
<table>
<tbody>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://medium.com/@thomas.laforge"><img src="https://avatars.githubusercontent.com/u/30832608?s…00&u=6f0ad9676792f29fd7fe6e113df06213d384a813&v=4" width="100px;" alt="Thomas Laforge"/><br /><sub><b>Thomas Laforge</b></sub></a><br />16 🧩</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://medium.com/@thomas.laforge"><img src="https://avatars.githubusercontent.com/u/30832608?s…00&u=6f0ad9676792f29fd7fe6e113df06213d384a813&v=4" width="100px;" alt="Thomas Laforge"/><br /><sub><b>Thomas Laforge</b></sub></a><br />17 🧩</a></td>
</tr>
</tbody>
</table>

View File

@@ -0,0 +1,36 @@
{
"extends": ["../../.eslintrc.json"],
"ignorePatterns": ["!**/*"],
"overrides": [
{
"files": ["*.ts"],
"rules": {
"@angular-eslint/directive-selector": [
"error",
{
"type": "attribute",
"prefix": "app",
"style": "camelCase"
}
],
"@angular-eslint/component-selector": [
"error",
{
"type": "element",
"prefix": "app",
"style": "kebab-case"
}
]
},
"extends": [
"plugin:@nrwl/nx/angular",
"plugin:@angular-eslint/template/process-inline-templates"
]
},
{
"files": ["*.html"],
"extends": ["plugin:@nrwl/nx/angular-template"],
"rules": {}
}
]
}

View File

@@ -0,0 +1,37 @@
<h1>Router Testing</h1>
> Author: Thomas Laforge
### Information
Testing is a crucial step in building scalable, maintainable, and trustworthy applications.
Testing should never be avoided, even in the face of short deadlines or strong pressure from the product team.
Nowadays, there are numerous awesome tools available that make it easy to test your code and provide a great developer experience.
In this series of testing exercises, we will learn and master Testing Library that simplifies DOM manipulation for testing any Angular component.
### Statement:
We have a functional application that lists available books for searching. If the search is valid, you will be directed to one or more books, otherwise, you will end up on an error page.
The goal is to test this behavior.
A file named `app.component.spec.ts`
### Submitting your work
1. Fork the project
2. clone it
3. npm install
4. `npx nx serve router-testing` to play with the application
5. `npx nx test router-testing` to test your application
6. _...work on it_
7. Commit your work
8. Submit a PR with a title beginning with **Answer:17** that I will review and other dev can review.
<a href="https://github.com/tomalaforge/angular-challenges/pulls?q=label%3A17+label%3Aanswer"><img src="https://img.shields.io/badge/-Solutions-green" alt="router testing"/></a>
<!-- <a href='https://github.com/tomalaforge/angular-challenges/pulls?q=label%3A17+label%3A"answer+author"'><img src="https://img.shields.io/badge/-Author solution-important" alt="router testing solution author"/></a>
<a href="{Blog post url}" target="_blank" rel="noopener noreferrer"><img src="https://img.shields.io/badge/-Blog post explanation-blue" alt="router testing blog article"/></a> -->
_You can ask any question on_ <a href="https://twitter.com/laforge_toma" target="_blank" rel="noopener noreferrer"><img src="./../../logo/twitter.svg" height=20px alt="twitter"/></a>

View File

@@ -0,0 +1,22 @@
/* eslint-disable */
export default {
displayName: 'router-testing',
preset: '../../jest.preset.js',
setupFilesAfterEnv: ['<rootDir>/src/test-setup.ts'],
globals: {},
transform: {
'^.+\\.(ts|mjs|js|html)$': [
'jest-preset-angular',
{
tsconfig: '<rootDir>/tsconfig.spec.json',
stringifyContentPathRegex: '\\.(html|svg)$',
},
],
},
transformIgnorePatterns: ['node_modules/(?!.*\\.mjs$)'],
snapshotSerializers: [
'jest-preset-angular/build/serializers/no-ng-attributes',
'jest-preset-angular/build/serializers/ng-snapshot',
'jest-preset-angular/build/serializers/html-comment',
],
};

View File

@@ -0,0 +1,90 @@
{
"name": "router-testing",
"$schema": "../../node_modules/nx/schemas/project-schema.json",
"projectType": "application",
"sourceRoot": "apps/router-testing/src",
"prefix": "app",
"targets": {
"build": {
"executor": "@angular-devkit/build-angular:browser",
"outputs": ["{options.outputPath}"],
"options": {
"outputPath": "dist/apps/router-testing",
"index": "apps/router-testing/src/index.html",
"main": "apps/router-testing/src/main.ts",
"polyfills": ["zone.js"],
"tsConfig": "apps/router-testing/tsconfig.app.json",
"inlineStyleLanguage": "scss",
"assets": [
"apps/router-testing/src/favicon.ico",
"apps/router-testing/src/assets"
],
"styles": ["apps/router-testing/src/styles.scss"],
"scripts": []
},
"configurations": {
"production": {
"budgets": [
{
"type": "initial",
"maximumWarning": "500kb",
"maximumError": "1mb"
},
{
"type": "anyComponentStyle",
"maximumWarning": "2kb",
"maximumError": "4kb"
}
],
"outputHashing": "all"
},
"development": {
"buildOptimizer": false,
"optimization": false,
"vendorChunk": true,
"extractLicenses": false,
"sourceMap": true,
"namedChunks": true
}
},
"defaultConfiguration": "production"
},
"serve": {
"executor": "@angular-devkit/build-angular:dev-server",
"configurations": {
"production": {
"browserTarget": "router-testing:build:production"
},
"development": {
"browserTarget": "router-testing:build:development"
}
},
"defaultConfiguration": "development"
},
"extract-i18n": {
"executor": "@angular-devkit/build-angular:extract-i18n",
"options": {
"browserTarget": "router-testing:build"
}
},
"lint": {
"executor": "@nrwl/linter:eslint",
"outputs": ["{options.outputFile}"],
"options": {
"lintFilePatterns": [
"apps/router-testing/**/*.ts",
"apps/router-testing/**/*.html"
]
}
},
"test": {
"executor": "@nrwl/jest:jest",
"outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
"options": {
"jestConfig": "apps/router-testing/jest.config.ts",
"passWithNoTests": true
}
}
},
"tags": []
}

View File

@@ -0,0 +1,21 @@
describe('AppComponent', () => {
it('shows error message and disabled button because no search criteria are typed', async () => {
//todo
});
it('shows No book found because no book match the search', async () => {
//todo
});
it('shows One book because the search matches one book', async () => {
//todo
});
it('shows One book because the search matches one book even with different cases', async () => {
//todo
});
it('shows a list of books because the search matches multiples books', async () => {
//todo
});
});

View File

@@ -0,0 +1,10 @@
import { Component } from '@angular/core';
import { RouterOutlet } from '@angular/router';
@Component({
standalone: true,
imports: [RouterOutlet],
selector: 'app-root',
template: ` <router-outlet /> `,
})
export class AppComponent {}

View File

@@ -0,0 +1,18 @@
import { ActivatedRouteSnapshot, Route } from '@angular/router';
import { bookGuard } from './book.guard';
export const appRoutes: Route[] = [
{
path: '',
loadComponent: () => import('./search.component'),
},
{
path: 'shelf',
canActivate: [(route: ActivatedRouteSnapshot) => bookGuard(route)],
loadComponent: () => import('./shelf.component'),
},
{
path: 'no-result',
loadComponent: () => import('./no-book-search.component'),
},
];

View File

@@ -0,0 +1,20 @@
import { inject } from '@angular/core';
import { ActivatedRouteSnapshot, Router } from '@angular/router';
import { availableBooks } from './book.model';
export const bookGuard = (
route: ActivatedRouteSnapshot,
router = inject(Router)
) => {
const searchParam = route.queryParams?.['book'].toLowerCase();
const isBookAvailable =
!!searchParam &&
availableBooks.some(
(b) =>
b.author.toLowerCase().includes(searchParam) ||
b.name.toLowerCase().includes(searchParam)
);
return isBookAvailable || router.parseUrl('no-result');
};

View File

@@ -0,0 +1,17 @@
export interface Book {
name: string;
author: string;
}
export const availableBooks = [
{ name: 'To Kill a Mockingbird', author: 'Harper Lee' },
{ name: '1984', author: 'George Orwell' },
{ name: 'The Catcher in the Rye', author: 'J.D. Salinger' },
{ name: 'The Great Gats', author: 'F. Scott Fitzgerald' },
{ name: 'Pride and Prejudice', author: 'Jane Austen' },
{ name: 'The Hobbit', author: 'J.R.R. Tolkien' },
{ name: 'The Lord of the Rings', author: 'J.R.R. Tolkien' },
{ name: "Harry Potter and the Philosopher's Stone", author: 'J.K. Rowling' },
{ name: 'The Hunger Games', author: 'Suzanne Collins' },
{ name: 'Animal Farm', author: 'George Orwell' },
];

View File

@@ -0,0 +1,13 @@
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { RouterLink } from '@angular/router';
@Component({
standalone: true,
imports: [RouterLink],
template: `
<div>No book found for this search</div>
<button routerLink="/">Go Back</button>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export default class ShelfComponent {}

View File

@@ -0,0 +1,38 @@
import { NgFor, NgIf } from '@angular/common';
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { FormControl, ReactiveFormsModule, Validators } from '@angular/forms';
import { RouterLink } from '@angular/router';
import { availableBooks } from './book.model';
@Component({
standalone: true,
imports: [ReactiveFormsModule, RouterLink, NgFor, NgIf],
template: `
<div>
<label for="bookName">Choose Book by author or title</label>
<input
type="text"
id="bookName"
name="bookName"
[formControl]="searchBook"
required />
</div>
<div *ngIf="searchBook.errors">Search criteria is required!</div>
<button
routerLink="/shelf"
[queryParams]="{ book: searchBook.value }"
[disabled]="searchBook.errors"
routerLinkActive="router-link-active">
Get book
</button>
<div>List of books available:</div>
<ul>
<li *ngFor="let book of books">{{ book.name }} by {{ book.author }}</li>
</ul>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export default class SearchComponent {
searchBook = new FormControl('', Validators.required);
books = availableBooks;
}

View File

@@ -0,0 +1,33 @@
import { AsyncPipe, JsonPipe, NgFor } from '@angular/common';
import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
import { ActivatedRoute, RouterLink } from '@angular/router';
import { map } from 'rxjs';
import { availableBooks } from './book.model';
@Component({
selector: 'app-shelf',
standalone: true,
imports: [AsyncPipe, JsonPipe, RouterLink, NgFor],
template: `
<ul>
<li *ngFor="let book of books | async">
Book: {{ book.name }} by {{ book.author }}
</li>
</ul>
<button routerLink="/">Go Back</button>
`,
styles: [],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export default class ShelfComponent {
readonly books = inject(ActivatedRoute).queryParams.pipe(
map((params) => params?.['book'].toLowerCase()),
map((param) =>
availableBooks.filter(
(b) =>
b.name.toLowerCase().includes(param) ||
b.author.toLowerCase().includes(param)
)
)
);
}

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>RouterTesting</title>
<base href="/" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" type="image/x-icon" href="favicon.ico" />
</head>
<body>
<app-root></app-root>
</body>
</html>

View File

@@ -0,0 +1,8 @@
import { bootstrapApplication } from '@angular/platform-browser';
import { provideRouter } from '@angular/router';
import { AppComponent } from './app/app.component';
import { appRoutes } from './app/app.routes';
bootstrapApplication(AppComponent, {
providers: [provideRouter(appRoutes)],
}).catch((err) => console.error(err));

View File

@@ -0,0 +1 @@
/* You can add global styles to this file, and also import other style files */

View File

@@ -0,0 +1,2 @@
import '@testing-library/jest-dom';
import 'jest-preset-angular/setup-jest';

View File

@@ -0,0 +1,10 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "../../dist/out-tsc",
"types": []
},
"files": ["src/main.ts"],
"include": ["src/**/*.d.ts"],
"exclude": ["jest.config.ts", "src/**/*.test.ts", "src/**/*.spec.ts"]
}

View File

@@ -0,0 +1,7 @@
{
"extends": "./tsconfig.json",
"include": ["src/**/*.ts"],
"compilerOptions": {
"types": ["jest", "node"]
}
}

View File

@@ -0,0 +1,32 @@
{
"compilerOptions": {
"target": "es2022",
"useDefineForClassFields": false,
"forceConsistentCasingInFileNames": true,
"strict": true,
"noImplicitOverride": true,
"noPropertyAccessFromIndexSignature": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true
},
"files": [],
"include": [],
"references": [
{
"path": "./tsconfig.app.json"
},
{
"path": "./tsconfig.spec.json"
},
{
"path": "./tsconfig.editor.json"
}
],
"extends": "../../tsconfig.base.json",
"angularCompilerOptions": {
"enableI18nLegacyMessageIdFormat": false,
"strictInjectionParameters": true,
"strictInputAccessModifiers": true,
"strictTemplates": true
}
}

View File

@@ -0,0 +1,15 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "../../dist/out-tsc",
"module": "commonjs",
"types": ["jest", "node", "@testing-library/jest-dom"]
},
"files": ["src/test-setup.ts"],
"include": [
"jest.config.ts",
"src/**/*.test.ts",
"src/**/*.spec.ts",
"src/**/*.d.ts"
]
}

671
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -64,6 +64,8 @@
"@nrwl/workspace": "15.8.5",
"@schematics/angular": "15.2.1",
"@testing-library/angular": "^13.3.0",
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/user-event": "^14.4.3",
"@types/jest": "29.4.0",
"@types/node": "16.11.7",
"@typescript-eslint/eslint-plugin": "^5.36.1",