feat(test): rename project to feat upcoming challenges

This commit is contained in:
thomas
2023-04-08 13:36:57 +02:00
parent 3b45e10570
commit 95e0f6b5f8
31 changed files with 36 additions and 37 deletions

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,42 @@
<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](https://testing-library.com/docs/) and [Cypress Component Testing](https://docs.cypress.io/guides/component-testing/angular/overview) that simplifies DOM manipulation for testing any Angular component.
### Statement:
We have a functional application that lists available books for borrowing inside a library. If the book you searched is available, you will be directed to the corresponding book(s), otherwise, you will end up on an error page.
The goal is to test this behavior with Testing library and Cypress
The file named `app.component.spec.ts` will let test your application using Testing Library. To run the test suits, you need to run `npx nx test testing-router-outlet`. You can also install [Jest Runner](https://marketplace.visualstudio.com/items?itemName=firsttris.vscode-jest-runner) to execute your test by clicking on the `Run` button above each `describe` or `it` blocks.
For testing cypress, you will execute your test inside the `app.component.cy.ts` and run `npx nx component-test testing-router-outlet` to execute your test suits. You can add the `--watch` flag to execute your test in watch mode.
I created some `it` blocks but feel free to add more test if you like to.
### Submitting your work
1. Fork the project
2. clone it
3. npm install
4. `npx nx serve testing-router-outlet` to play with the application
5. `npx nx test testing-router-outlet` to test your application with Testing Library
6. `npx nx component-test testing-router-outlet --watch` to test your application with Cypress
7. _...work on it_
8. Commit your work
9. 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,6 @@
import { defineConfig } from 'cypress';
import { nxComponentTestingPreset } from '@nrwl/angular/plugins/component-testing';
export default defineConfig({
component: nxComponentTestingPreset(__filename),
});

View File

@@ -0,0 +1,5 @@
{
"name": "Using fixtures to represent data",
"email": "hello@cypress.io",
"body": "Fixtures are a great way to mock data for responses to routes"
}

View File

@@ -0,0 +1,42 @@
/// <reference types="cypress" />
import { mount } from 'cypress/angular';
// ***********************************************
// This example commands.ts shows you how to
// create various custom commands and overwrite
// existing commands.
//
// For more comprehensive examples of custom
// commands please read more here:
// https://on.cypress.io/custom-commands
// ***********************************************
declare global {
// eslint-disable-next-line @typescript-eslint/no-namespace
namespace Cypress {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
interface Chainable<Subject> {
login(email: string, password: string): void;
mount: typeof mount;
}
}
}
Cypress.Commands.add('mount', mount);
//
// -- This is a parent command --
Cypress.Commands.add('login', (email, password) => {
console.log('Custom command example: Login', email, password);
});
//
// -- This is a child command --
// Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... })
//
//
// -- This is a dual command --
// Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... })
//
//
// -- This will overwrite an existing command --
// Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... })

View File

@@ -0,0 +1,12 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
<title>router-testing Components App</title>
</head>
<body>
<div data-cy-root></div>
</body>
</html>

View File

@@ -0,0 +1,17 @@
// ***********************************************************
// This example support/component.ts is processed and
// loaded automatically before your test files.
//
// This is a great place to put global configuration and
// behavior that modifies Cypress.
//
// You can change the location of this file or turn off
// automatically serving support files with the
// 'supportFile' configuration option.
//
// You can read more here:
// https://on.cypress.io/configuration
// ***********************************************************
// Import commands.ts using ES2015 syntax:
import './commands';

View File

@@ -0,0 +1,17 @@
{
"extends": "../tsconfig.json",
"compilerOptions": {
"outDir": "../../../dist/out-tsc",
"module": "commonjs",
"types": ["cypress", "node"]
},
"include": [
"support/**/*.ts",
"../cypress.config.ts",
"../**/*.cy.ts",
"../**/*.cy.tsx",
"../**/*.cy.js",
"../**/*.cy.jsx",
"../**/*.d.ts"
]
}

View File

@@ -0,0 +1,22 @@
/* eslint-disable */
export default {
displayName: 'testing-router-outlet',
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,99 @@
{
"name": "testing-router-outlet",
"$schema": "../../node_modules/nx/schemas/project-schema.json",
"projectType": "application",
"sourceRoot": "apps/testing-router-outlet/src",
"prefix": "app",
"targets": {
"build": {
"executor": "@angular-devkit/build-angular:browser",
"outputs": ["{options.outputPath}"],
"options": {
"outputPath": "dist/apps/testing-router-outlet",
"index": "apps/testing-router-outlet/src/index.html",
"main": "apps/testing-router-outlet/src/main.ts",
"polyfills": ["zone.js"],
"tsConfig": "apps/testing-router-outlet/tsconfig.app.json",
"inlineStyleLanguage": "scss",
"assets": [
"apps/testing-router-outlet/src/favicon.ico",
"apps/testing-router-outlet/src/assets"
],
"styles": ["apps/testing-router-outlet/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": "testing-router-outlet:build:production"
},
"development": {
"browserTarget": "testing-router-outlet:build:development"
}
},
"defaultConfiguration": "development"
},
"extract-i18n": {
"executor": "@angular-devkit/build-angular:extract-i18n",
"options": {
"browserTarget": "testing-router-outlet:build"
}
},
"lint": {
"executor": "@nrwl/linter:eslint",
"outputs": ["{options.outputFile}"],
"options": {
"lintFilePatterns": [
"apps/testing-router-outlet/**/*.ts",
"apps/testing-router-outlet/**/*.html"
]
}
},
"test": {
"executor": "@nrwl/jest:jest",
"outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
"options": {
"jestConfig": "apps/testing-router-outlet/jest.config.ts",
"passWithNoTests": true
}
},
"component-test": {
"executor": "@nrwl/cypress:cypress",
"options": {
"cypressConfig": "apps/testing-router-outlet/cypress.config.ts",
"testingType": "component",
"skipServe": true,
"devServerTarget": "testing-router-outlet:build"
}
}
},
"tags": []
}

View File

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

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,42 @@
import { Component } from '@angular/core';
import { RouterLink, RouterOutlet } from '@angular/router';
@Component({
standalone: true,
imports: [RouterOutlet, RouterLink],
selector: 'app-root',
styles: [
`
h1 {
margin-bottom: 0;
}
nav a {
padding: 1rem;
text-decoration: none;
margin-top: 10px;
display: inline-block;
background-color: #e8e8e8;
color: #3d3d3d;
border-radius: 4px;
margin-bottom: 10px;
}
nav a:hover {
color: white;
background-color: #42545c;
}
nav a.active {
background-color: black;
}
`,
],
template: `
<h1>Library</h1>
<nav>
<a routerLink="/search" routerLinkActive="active">Borrow a Book</a>
</nav>
<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: 'search',
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,8 @@
import { ChangeDetectionStrategy, Component } from '@angular/core';
@Component({
standalone: true,
template: ` <div>No book found for this search</div> `,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export default class ShelfComponent {}

View File

@@ -0,0 +1,66 @@
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],
styles: [
`
:host {
display: flex;
flex-direction: column;
gap: 10px;
}
.error {
color: red;
}
button {
width: 300px;
padding: 5px;
border-radius: 5px;
}
.search label {
margin-right: 15px;
}
`,
],
template: `
<div class="search">
<label for="bookName">Search Book by author or title</label>
<input
type="text"
id="bookName"
name="bookName"
[formControl]="searchBook"
required />
<div class="error" *ngIf="searchBook.errors">
Search criteria is required!
</div>
</div>
<button
data-cy="borrow-btn"
routerLink="/shelf"
[queryParams]="{ book: searchBook.value }"
[disabled]="searchBook.errors"
routerLinkActive="router-link-active">
Borrow
</button>
<div>
<h3>List of books available:</h3>
<ul>
<li *ngFor="let book of books">{{ book.name }} by {{ book.author }}</li>
</ul>
</div>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export default class SearchComponent {
searchBook = new FormControl('', Validators.required);
books = availableBooks;
}

View File

@@ -0,0 +1,32 @@
import { AsyncPipe, JsonPipe, NgFor } from '@angular/common';
import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { map } from 'rxjs';
import { availableBooks } from './book.model';
@Component({
selector: 'app-shelf',
standalone: true,
imports: [AsyncPipe, JsonPipe, NgFor],
template: `
<ul>
<li *ngFor="let book of books | async">
Borrowed Book: {{ book.name }} by {{ book.author }}
</li>
</ul>
`,
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)
)
)
);
}

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,20 @@
{
"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",
"cypress/**/*",
"cypress.config.ts",
"**/*.cy.ts",
"**/*.cy.js",
"**/*.cy.tsx",
"**/*.cy.jsx"
]
}

View File

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

View File

@@ -0,0 +1,35 @@
{
"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"
},
{
"path": "./cypress/tsconfig.cy.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"
]
}