mirror of
https://github.com/Raghu-Ch/angular-challenges.git
synced 2026-02-10 04:43:03 -05:00
feat(challenge17): router testing
This commit is contained in:
@@ -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>
|
||||
|
||||
36
apps/router-testing/.eslintrc.json
Normal file
36
apps/router-testing/.eslintrc.json
Normal 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": {}
|
||||
}
|
||||
]
|
||||
}
|
||||
37
apps/router-testing/README.md
Normal file
37
apps/router-testing/README.md
Normal 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>
|
||||
22
apps/router-testing/jest.config.ts
Normal file
22
apps/router-testing/jest.config.ts
Normal 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',
|
||||
],
|
||||
};
|
||||
90
apps/router-testing/project.json
Normal file
90
apps/router-testing/project.json
Normal 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": []
|
||||
}
|
||||
21
apps/router-testing/src/app/app.component.spec.ts
Normal file
21
apps/router-testing/src/app/app.component.spec.ts
Normal 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
|
||||
});
|
||||
});
|
||||
10
apps/router-testing/src/app/app.component.ts
Normal file
10
apps/router-testing/src/app/app.component.ts
Normal 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 {}
|
||||
18
apps/router-testing/src/app/app.routes.ts
Normal file
18
apps/router-testing/src/app/app.routes.ts
Normal 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'),
|
||||
},
|
||||
];
|
||||
20
apps/router-testing/src/app/book.guard.ts
Normal file
20
apps/router-testing/src/app/book.guard.ts
Normal 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');
|
||||
};
|
||||
17
apps/router-testing/src/app/book.model.ts
Normal file
17
apps/router-testing/src/app/book.model.ts
Normal 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' },
|
||||
];
|
||||
13
apps/router-testing/src/app/no-book-search.component.ts
Normal file
13
apps/router-testing/src/app/no-book-search.component.ts
Normal 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 {}
|
||||
38
apps/router-testing/src/app/search.component.ts
Normal file
38
apps/router-testing/src/app/search.component.ts
Normal 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;
|
||||
}
|
||||
33
apps/router-testing/src/app/shelf.component.ts
Normal file
33
apps/router-testing/src/app/shelf.component.ts
Normal 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)
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
0
apps/router-testing/src/assets/.gitkeep
Normal file
0
apps/router-testing/src/assets/.gitkeep
Normal file
BIN
apps/router-testing/src/favicon.ico
Normal file
BIN
apps/router-testing/src/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
13
apps/router-testing/src/index.html
Normal file
13
apps/router-testing/src/index.html
Normal 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>
|
||||
8
apps/router-testing/src/main.ts
Normal file
8
apps/router-testing/src/main.ts
Normal 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));
|
||||
1
apps/router-testing/src/styles.scss
Normal file
1
apps/router-testing/src/styles.scss
Normal file
@@ -0,0 +1 @@
|
||||
/* You can add global styles to this file, and also import other style files */
|
||||
2
apps/router-testing/src/test-setup.ts
Normal file
2
apps/router-testing/src/test-setup.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
import '@testing-library/jest-dom';
|
||||
import 'jest-preset-angular/setup-jest';
|
||||
10
apps/router-testing/tsconfig.app.json
Normal file
10
apps/router-testing/tsconfig.app.json
Normal 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"]
|
||||
}
|
||||
7
apps/router-testing/tsconfig.editor.json
Normal file
7
apps/router-testing/tsconfig.editor.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"include": ["src/**/*.ts"],
|
||||
"compilerOptions": {
|
||||
"types": ["jest", "node"]
|
||||
}
|
||||
}
|
||||
32
apps/router-testing/tsconfig.json
Normal file
32
apps/router-testing/tsconfig.json
Normal 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
|
||||
}
|
||||
}
|
||||
15
apps/router-testing/tsconfig.spec.json
Normal file
15
apps/router-testing/tsconfig.spec.json
Normal 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
671
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user