Angular 12 : de TSLint à ESLint et de Protractor à Cypress

Publié le

Angular 12 a modernisé plusieurs de ses outils : je vous explique comment mettre à jour vos projets vers ESLint et Cypress.

De TSLint à ESLint

TSLint n’est plus installé dans les nouveaux projets. C’est à vous d’ajouter ESLint :

ng add @angular-eslint/schematics

Pour les projets existants, pour chaque application et chaque librairie :

ng g @angular-eslint/schematics:convert-tslint-to-eslint --remove-tslint-if-no-more-tslint-targets --project nomduprojetdansangularjson --ignore-existing-tslint-config

La dernière option est facultative si vous souhaitez garder des règles customisées venant de votre configuration TSLint, mais cela risque d’ajouter des packages supplémentaires et non « officiels » pour ESLint.

Dans tous les cas, la configuration d’ESLint que vous obtiendrez avec ces commandes sera très pauvre et loin de ce que nous avions mis en place pendant la formation. Vous perdrez notamment la distinction entre composants Page et Component. Partez plutôt sur la base du .eslintrc.json suivant (à adapter éventuellement) :

{
"root": true,
"ignorePatterns": [
"projects/**/*"
],
"overrides": [
{
"files": [
"*.ts"
],
"parserOptions": {
"project": [
"tsconfig.json"
],
"createDefaultProgram": true
},
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"plugin:@typescript-eslint/recommended-requiring-type-checking",
"plugin:@angular-eslint/recommended",
"plugin:@angular-eslint/template/process-inline-templates"
],
"rules": {
// Good practice to differentiate component types
"@angular-eslint/component-class-suffix": [
"error",
{
"suffixes": [
"Component",
"Page"
]
}
],
// Prefixes
"@angular-eslint/component-selector": [
"error",
{
"type": "element",
"prefix": "app",
"style": "kebab-case"
}
],
"@angular-eslint/directive-selector": [
"error",
{
"type": "attribute",
"prefix": "app",
"style": "camelCase"
}
],
// Strict types
"@typescript-eslint/no-explicit-any": "error",
"@typescript-eslint/explicit-module-boundary-types": "error",
// Stricter Angular ESLint rules
"@angular-eslint/contextual-decorator": "error",
"@angular-eslint/no-attribute-decorator": "error",
"@angular-eslint/no-forward-ref": "error",
"@angular-eslint/no-input-prefix": "error",
"@angular-eslint/no-lifecycle-call": "error",
"@angular-eslint/no-pipe-impure": "error",
"@angular-eslint/no-queries-metadata-property": "error",
"@angular-eslint/use-component-view-encapsulation": "error",
"@angular-eslint/use-injectable-provided-in": "error",
// Annoying as Angular CLI generates empty constructors and `ngOnInit()`
"@typescript-eslint/no-empty-function": "off",
"@angular-eslint/no-empty-lifecycle-method": "off",
// Allow Angular forms validators like `Validator.required`
"@typescript-eslint/unbound-method": [
"error",
{
"ignoreStatic": true
}
],
// Formatting rules (can be removed when using a dedicated tool like Prettier)
"quotes": "off",
"@typescript-eslint/quotes": [
"warn",
"single",
{
"allowTemplateLiterals": true
}
]
}
},
{
"files": [
"*.html"
],
"extends": [
"plugin:@angular-eslint/template/recommended"
],
"rules": {
// Strict types
"@angular-eslint/template/no-any": "error",
// Stricter Angular ESLint rules
"@angular-eslint/template/conditional-complexity": "error",
"@angular-eslint/template/cyclomatic-complexity": "error",
"@angular-eslint/template/no-call-expression": "error",
"@angular-eslint/template/no-duplicate-attributes": "error",
// Accessibility
"@angular-eslint/template/accessibility-alt-text": "error",
"@angular-eslint/template/accessibility-elements-content": "error",
"@angular-eslint/template/accessibility-table-scope": "error",
"@angular-eslint/template/accessibility-valid-aria": "error",
"@angular-eslint/template/click-events-have-key-events": "error",
"@angular-eslint/template/mouse-events-have-key-events": "error",
"@angular-eslint/template/no-autofocus": "error",
"@angular-eslint/template/no-distracting-elements": "error",
"@angular-eslint/template/no-positive-tabindex": "error",
"@angular-eslint/template/accessibility-label-for": "error"
}
}
]
}
view raw .eslintrc.json hosted with ❤ by GitHub

De Protractor à Cypress

Protractor est toujours fonctionnel mais n’est plus installé par défaut dans les nouveaux projets Angular. Il devrait être définitivement déprécié dans Angular 15. Vous pouvez suivre l’évolution de ce sujet dans la RFC officielle.

Il existe plusieurs alternatives, chacune avec ses avantages et ses inconvénients, par exemple :

  • Cypress : à la mode et installation facile, mais ne sait pas gérer Safari pour l’instant
  • Playwright : nativement en TypeScript car géré par Microsoft, fiable techniquement mais très récent donc installation et documentation encore un peu laborieuses

Nous allons voir comment installer et configurer Cypress :

ng add @cypress/schematic

Lorsqu’on vous demande si vous voulez mettre à jour la commande ng e2e, dites oui si vous avez peu de tests et que vous pouvez les migrer en une seule fois, non si vous en avez beaucoup et que vous voulez faire la migration progressivement.

Cette commande fait l’essentiel, mais il est préférable d’ajuster d’autres configurations :

  • .gitignore : Cypress produit par défaut des enregistrements vidéos des tests, qu’il vaut mieux ne pas committer, vous pouvez par exemple ignorer le répertoire cypress/videos
  • .eslintrc.json : dans ignorePatterns, ajoutez cypress/**/*
  • npm install eslint-plugin-cypress -D
  • cypress/.eslintrc.json : ajoutez ce fichier avec la configuration suivante :
{
"root": true,
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"plugin:cypress/recommended"
]
}
view raw .eslintrc.json hosted with ❤ by GitHub

Il ne vous reste plus qu’à écrire vos tests e2e dans le répertoire cypress/integration. Par exemple, avant avec Protractor :

import { browser, $, ExpectedConditions } from 'protractor';
describe('Account', () => {
it('should register successfully', async () => {
const email = `test_${Date.now()}@example.com`;
const password = 'test';
await browser.get('/account/register');
await $('input[type="email"]').sendKeys(email);
await $('input[type="password"]').sendKeys(password);
await $('button[type="submit"]').click();
await browser.wait(ExpectedConditions.urlContains('/account/login'));
});
});
view raw app.e2e-spec.ts hosted with ❤ by GitHub

Après avec Cypress :

describe('Account', () => {
it('should register successfully', () => {
const email = `test_${Date.now()}@example.com`;
const password = 'test';
cy.visit('/account/register');
cy.get('input[type="email"]').type(email);
cy.get('input[type="password"]').type(password);
cy.get('button[type="submit"]').click();
cy.url().should('include', '/account/login');
});
});
view raw spec.ts hosted with ❤ by GitHub

Comme vous pouvez le voir, la logique est exactement la même, seule la syntaxe diffère. Vous trouverez d’autres exemples dans le guide de migration de Cypress.

Pour lancer vos tests, 2 possibilités :

  • ng e2e ou ng run nomdevotreprojetdansangularjson:cypress-open : en mode visuel
  • ng run nomdevotreprojetdansangularjson:cypress-run : en mode ligne de commande (pour le CI)

Vous pouvez mettre à jour vos tests progressivement. Une fois terminé, vous pouvez supprimer les packages suivants :

  • protractor
  • ts-node

Découvrez la formation Angular