|
|
|
@ -0,0 +1,446 @@
|
|
|
|
|
---
|
|
|
|
|
gitea: none
|
|
|
|
|
|
|
|
|
|
include_toc: true
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
# Applications et tests
|
|
|
|
|
|
|
|
|
|
[TOC]
|
|
|
|
|
|
|
|
|
|
## Applications Web
|
|
|
|
|
|
|
|
|
|
Comme vous le savez déjà, une application Web est composée de :
|
|
|
|
|
|
|
|
|
|
- une partie **cliente** (*frontend*), gérant l'interface graphique (GUI) et les interactions avec l'utilisateur.
|
|
|
|
|
|
|
|
|
|
- une partie **serveur** (*backend*), gérant à la fois les ressources statiques (fichiers HTML/CSS/JS) et les ressources dynamiques (API REST).
|
|
|
|
|
|
|
|
|
|
le frontend est exécuté sur le navigateur, i.e. la même application, les mêmes langages, pour les différentes plateformes desktop/mobile.
|
|
|
|
|
|
|
|
|
|
Il est cependant fréquent de déporter des parties du serveur vers le client :
|
|
|
|
|
|
|
|
|
|
- des **calculs** afin de réduire la charge du serveur et éviter certaines attaques DoS.
|
|
|
|
|
|
|
|
|
|
- des **fichiers statiques** via des [politiques de cache](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control), afin de réduire la consommation et les besoins en bande passante.
|
|
|
|
|
|
|
|
|
|
- des **données** via `localStorage` ou `indexDB`, notamment pour des raisons de vie privée.
|
|
|
|
|
|
|
|
|
|
Pour certaines applications, il arrive aussi qu'on souhaite gérer des pertes de connexions temporaires. Par exemple en ayant un "faux" serveur côté client, e.g. via un [ServiceWorker](https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API), interceptant les requêtes REST, et maintenant un état interne local, pour ensuite se synchroniser avec le serveur distant une fois la connexion rétablie.
|
|
|
|
|
|
|
|
|
|
Mais on peut aller plus loin, en installant le serveur côté client, c'est à dire que l'ordinateur client exécutera lui-même :
|
|
|
|
|
|
|
|
|
|
- *le frontend:* i.e. un navigateur permettant de visualiser les pages Web.
|
|
|
|
|
|
|
|
|
|
- un serveur Web pour distribuer les fichiers statiques au frontend.
|
|
|
|
|
|
|
|
|
|
- le *backend*: un serveur HTTP répondant aux requêtes du client (e.g. API REST ou websockets).
|
|
|
|
|
|
|
|
|
|
Cela possède plusieurs avantages :
|
|
|
|
|
|
|
|
|
|
- peu de différences entre les versions web, desktop, et mobile d'une application.
|
|
|
|
|
|
|
|
|
|
- grande liberté dans le design des interfaces graphiques grâce à HTML5/CSS3/JS.
|
|
|
|
|
|
|
|
|
|
- accéder aux ressources locales inaccessibles à partir du navigateur, e.g. le système de fichiers du client.
|
|
|
|
|
|
|
|
|
|
- temps de chargements des ressources réduites car locales, complètement hors ligne.
|
|
|
|
|
|
|
|
|
|
- on n'expose pas de données sensibles car conservées localement.
|
|
|
|
|
|
|
|
|
|
Dans ce cadre, on préfère généralement lancer une fenêtre n'affichant que le site Web, sans intégrer l'interface graphique du navigateur (e.g. barre d'URL, menus, etc.). On peut alors utiliser des frameworks comme [Electon](https://www.electronjs.org/) ou [Tauri](https://tauri.app/), mais le plus simple reste de lancer le navigateur avec les options suivantes :
|
|
|
|
|
|
|
|
|
|
```shell
|
|
|
|
|
chromium --app=$URL
|
|
|
|
|
firefox --kiosk $URL # force aussi le plein écran
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
## Validation de données à l'exécution
|
|
|
|
|
|
|
|
|
|
Afin de rendre l'application plus stable, d'éviter des bugs, et des comportements indéterminés, il est important de valider les données avant tout traitements. Or, si TypeScript permet de s'assurer du type des variables lors de la phase de compilation, il ne permet pas de s'assurer de leur validité en cours d'exécution. En effet, de nombreuses données peuvent ne pas être connues lors de la phase de compilation, issus notamment :
|
|
|
|
|
|
|
|
|
|
- de formulaires renseignés par l'utilisateur.
|
|
|
|
|
|
|
|
|
|
- de fichiers, qui peuvent être modifiés par un tiers, ou dont le format peut évoluer entre deux versions.
|
|
|
|
|
|
|
|
|
|
- de ressources distantes, e.g. via une API REST.
|
|
|
|
|
|
|
|
|
|
Lors des échanges client-serveur, il est aussi d'usage de valider les données 2 fois :
|
|
|
|
|
|
|
|
|
|
- *côté client* : pour ne pas inutilement envoyer des données invalides, et ainsi éviter de charger le serveur pour rien.
|
|
|
|
|
|
|
|
|
|
- *côté serveur* : car il ne faut **jamais faire confiance au client** : un tiers pouvant aisément modifier son comportement (e.g. via des WebExtensions, injections de scripts, forger des requêtes avec `curl`, etc.).
|
|
|
|
|
|
|
|
|
|
### Validation de formulaires
|
|
|
|
|
|
|
|
|
|
Avec HTML5/CSS3, il est possible de s'assurer de la validité des données renseignées dans un champ grâce aux attributs suivants :
|
|
|
|
|
|
|
|
|
|
- `required` : ce champ ne peut être vide.
|
|
|
|
|
|
|
|
|
|
- `type` : le type de données attendues (e.g. nombre, mails, etc.), [cf doc](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#input_types).
|
|
|
|
|
|
|
|
|
|
- `pattern` : une expression régulière (regex).
|
|
|
|
|
|
|
|
|
|
- `min`, `max`, `minlenght`, `maxlenght`.
|
|
|
|
|
|
|
|
|
|
Il est possible de vérifier la validité des données lors de 3 événements :
|
|
|
|
|
|
|
|
|
|
- `input` : la donnée est modifiée, usuellement, une indication visuelle (CSS) indique si la donnée est valide ou non.
|
|
|
|
|
|
|
|
|
|
- `change` : l'utilisateur quitte le champ (presse entré, clic ailleurs), usuellement, un message d'avertissement s'affiche si la donnée est invalide.
|
|
|
|
|
|
|
|
|
|
💡 On peut aussi activer et désactiver le bouton de soumission du formulaire en fonction de la présence ou non d'un champ dont la donnée est invalide.
|
|
|
|
|
|
|
|
|
|
- `submit` : le formulaire est soumis, usuellement, on valide la *structure des données* du formulaire, si invalide, on annule l'événement : `ev.preventDefault()`.
|
|
|
|
|
|
|
|
|
|
Vous pouvez aussi personnaliser l'affichage via :
|
|
|
|
|
|
|
|
|
|
- `:valid` pour ajouter des règles CSS aux champs ayant une donnée valide.
|
|
|
|
|
|
|
|
|
|
- `:invalid` pour ajouter des règles CSS aux champs ayant une donnée invalide.
|
|
|
|
|
|
|
|
|
|
- `.setCustomValidity(msg: string)` pour changer l'avertissement qui s'affiche.
|
|
|
|
|
|
|
|
|
|
Vous pouvez ainsi définir vos propres règles de validation :
|
|
|
|
|
|
|
|
|
|
```ts
|
|
|
|
|
input.addEventListener('input', () => {
|
|
|
|
|
// see also https://developer.mozilla.org/en-US/docs/Web/API/ValidityState
|
|
|
|
|
if( this.validity.valid === false ) { // invalide.
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if( ... ) // condition personnalisée.
|
|
|
|
|
return this.setCustomValidity('Check1 failed');
|
|
|
|
|
// ...
|
|
|
|
|
this.setCustomValidity(''); // pas d'erreurs.
|
|
|
|
|
})
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### Validation de structures de données
|
|
|
|
|
|
|
|
|
|
Plusieurs bibliothèques permettent de valider les structures de données, dont [SuperStruct](https://docs.superstructjs.org/) :
|
|
|
|
|
|
|
|
|
|
```ts
|
|
|
|
|
import * as ss from 'superstruct';
|
|
|
|
|
|
|
|
|
|
// Décrire la structure de données.
|
|
|
|
|
const UserStruct = ss.object({
|
|
|
|
|
id : ss.number(),
|
|
|
|
|
name: ss.string()
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Récupérer un type TS à partir de la description.
|
|
|
|
|
type User = ss.Infer<UserStruct>;
|
|
|
|
|
|
|
|
|
|
// Utilisation (validation d'un formulaire):
|
|
|
|
|
const formData = new FormData(form); // récupère les valeurs d'un formulaire.
|
|
|
|
|
const obj = Object.fromEntries(formData.entries()); // convertir en objet.
|
|
|
|
|
|
|
|
|
|
if( ss.is(obj, UserStruct) ) {} // retourne false si 'obj' ne correspond pas à UserStruct.
|
|
|
|
|
ss.assert(obj, UserStruct); // lance une exception si 'obj' ne correspond pas à UserStruct.
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
SuperStruct propose les types suivants :
|
|
|
|
|
|
|
|
|
|
- `literal(val)`, `boolean()`, `number()`, `integer()`, `date()`;
|
|
|
|
|
|
|
|
|
|
- `array(e)`, `tupple([])`, `set(v)`, `map(k,v)`, `record(k, v)` ;
|
|
|
|
|
|
|
|
|
|
- `object({})`, `type({})` `instance(Klass)`, `unknown()`, `never()`, `any()` ;
|
|
|
|
|
|
|
|
|
|
- `func()`, `regexp()`,` enums([])`.
|
|
|
|
|
|
|
|
|
|
💡 SuperStruct permet aussi de définir ses propres types :
|
|
|
|
|
|
|
|
|
|
```ts
|
|
|
|
|
const mytype = ss.define<Type>('$Name', (value) => ...)
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
SuperStruct permet aussi d'ajouter des précisions sur les types :
|
|
|
|
|
|
|
|
|
|
- `nullable(a)`, `optionnal(a)` ;
|
|
|
|
|
|
|
|
|
|
- `empty(val)`, `nonempty(val)`, `min(val, $min)`, `max(val, $max)`, `size(val, $size)` ;
|
|
|
|
|
|
|
|
|
|
- `intersection([])`, `union([])` ;
|
|
|
|
|
|
|
|
|
|
- `pattern(val, $regexp)` .
|
|
|
|
|
|
|
|
|
|
💡 SuperStruct permet aussi de rafiner des types existants :
|
|
|
|
|
|
|
|
|
|
```ts
|
|
|
|
|
const mycstrnt = ss.refine(Struct, '$Name', (value) => {
|
|
|
|
|
if( ... )
|
|
|
|
|
return "msg error";
|
|
|
|
|
return true;
|
|
|
|
|
});
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
💡 SuperStruct permet aussi de :
|
|
|
|
|
|
|
|
|
|
- [manipuler des descriptions](https://docs.superstructjs.org/api-reference/utilities) (partial, pick, omit) ;
|
|
|
|
|
|
|
|
|
|
- [pré-traiter les données](https://docs.superstructjs.org/api-reference/coercions) (valeurs par défaut, enlever les espaces, etc.) ;
|
|
|
|
|
|
|
|
|
|
- [définir des dépréciations](https://docs.superstructjs.org/api-reference/utilities#deprecated).
|
|
|
|
|
|
|
|
|
|
### Les expressions régulières (regex)
|
|
|
|
|
|
|
|
|
|
Les *expressions régulières* (regex) permettent de décrire un format en vue d'effectuer des opérations sur des chaînes de charactères.
|
|
|
|
|
|
|
|
|
|
```ts
|
|
|
|
|
// déclarer une regex.
|
|
|
|
|
const regexp = /^(yes|no)$/sm;
|
|
|
|
|
// ou
|
|
|
|
|
const regexp = new RegExp("^(yes|no)$", "sm");
|
|
|
|
|
|
|
|
|
|
// retourne un booléen indiquant si le string correspond au regex.
|
|
|
|
|
regexp.test('yes'); // true
|
|
|
|
|
regexp.test('non'); // false
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
| Expression | Description |
|
|
|
|
|
| ---------- | -------------------------------- |
|
|
|
|
|
| `^` | Début de la chaîne de caractère. |
|
|
|
|
|
| `$` | Fin de la chaîne de caractère. |
|
|
|
|
|
| `(ab\|cd)` | `ab` ou `cd` |
|
|
|
|
|
|
|
|
|
|
Il est possible d'utiliser des expressions régulières sur les opérations suivantes :
|
|
|
|
|
|
|
|
|
|
| Méthode | Description |
|
|
|
|
|
| ------------------ | ------------------------------------------------------------------------------------------------------------ |
|
|
|
|
|
| `.replace(r, v)` | Remplace la sous-chaîne correspondant au regex `r` par la valeur `v` (possibilité d'utiliser un callback). |
|
|
|
|
|
| `.replaceAll(r,v)` | Remplace les sous-chaînes correspondant au regex `r` par la valeur `v` (possibilité d'utiliser un callback). |
|
|
|
|
|
| `.search(r)` | Renvoie l'index de la première sous-chaîne correspondant à la regex `r`. |
|
|
|
|
|
| `.split(r)` | Découpe la chaîne de caractère en utilisant la regex `r` comme séparateur. |
|
|
|
|
|
|
|
|
|
|
💡 Vous pouvez visualiser et tester vos expression régulières, e.g. via [Regex Vis](https://regex-vis.com/).
|
|
|
|
|
|
|
|
|
|
L'onglet "Test" dans la colonne de droite, permet de tester votre Regexp sur plusieurs valeurs.
|
|
|
|
|
|
|
|
|
|
#### Classes de caractères
|
|
|
|
|
|
|
|
|
|
| Expression | Équivalance | Opposé | Description |
|
|
|
|
|
| ---------- | ------------- | --------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
|
|
|
| `.` | | | N'importe quel caractère. |
|
|
|
|
|
| `[abc]` | | `[^abc]` | Le caractère `a`, `b`, ou `c`. |
|
|
|
|
|
| `[a-d]` | `[abcd]` | `[^a-d]` | Caractère entre `a` et `d` (inclus). |
|
|
|
|
|
| `[-a-d]` | `[-abcd]` | `[^-a-d]` | Idem, mais avec `-` en plus. |
|
|
|
|
|
| `\d` | `[0-9]` | `\D` | Chiffre |
|
|
|
|
|
| `\w` | `[a-zA-Z0-9]` | `\W` | Caractère alpha-numérique. |
|
|
|
|
|
| `\s` | | `\S` | Espace |
|
|
|
|
|
| `\p{x}` | | `\P{x}` | Classes de charactères unicode.<br/>[cf documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Regular_expressions/Unicode_character_class_escape) |
|
|
|
|
|
|
|
|
|
|
#### Quantifieurs
|
|
|
|
|
|
|
|
|
|
Il permettent d'indiquer le nombre de répétitions d'un élément :
|
|
|
|
|
|
|
|
|
|
| Expression | Équivalance | Description |
|
|
|
|
|
| ---------- | ----------- | ---------------------------- |
|
|
|
|
|
| `{n,m}` | | entre `n` et `m` répétitions |
|
|
|
|
|
| `{n}` | `{n,n}` | `n` répétitions |
|
|
|
|
|
| `{n,}` | `{n,∞}` | au moins `n` répétitions |
|
|
|
|
|
| `?` | `{0,1}` | élément optionnel |
|
|
|
|
|
| `+` | `{1,∞}` | au moins une répétition |
|
|
|
|
|
| `*` | `{0,∞}` | élément répété ou optionnel. |
|
|
|
|
|
|
|
|
|
|
### Capture
|
|
|
|
|
|
|
|
|
|
Il est aussi possible de d'utiliser les regex pour extraire des valeurs d'une chaîne de caractère grâce à `(?<name>pattern)`.
|
|
|
|
|
|
|
|
|
|
```ts
|
|
|
|
|
const search = /<(?<name>.*)>/sm;
|
|
|
|
|
|
|
|
|
|
const result = search.exec("Une <balise> html.");
|
|
|
|
|
if( result !== null) { // le pattern a été trouvé
|
|
|
|
|
result.groups.name // = "balise".
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
⚠ En Python, la notation est `(?P<name>pattern)`
|
|
|
|
|
|
|
|
|
|
```python
|
|
|
|
|
import re
|
|
|
|
|
regex = re.compile("<(?P<name>.*)>", re.S|re.M)
|
|
|
|
|
result = re.match(regex, "Une <balise> html.")
|
|
|
|
|
if result is not None:
|
|
|
|
|
result.group('name') # "balise"
|
|
|
|
|
# result.groupdict() pour obtenir l'ensemble des groupe sous forme de dict.
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
## Les tests
|
|
|
|
|
|
|
|
|
|
Valider le type et la structure des données n'est pas suffisant pour garantir le bon fonctionnement d'une application. On écrit alors des tests notamment pour :
|
|
|
|
|
|
|
|
|
|
- garantir que l'application respecte le comportement défini dans *le cahier des charges* ;
|
|
|
|
|
|
|
|
|
|
- *développement dirigé par les tests* (TDD), i.e. les tests servent de *cahier des charges*.
|
|
|
|
|
|
|
|
|
|
- prévenir des *régressions*, i.e., suite à une modification, un bug (re-)introduit sur une fonctionnalité existante ;
|
|
|
|
|
|
|
|
|
|
- *servir de documentation*, donne des exemples d'usages des fonctions.
|
|
|
|
|
|
|
|
|
|
- *déboguer*.
|
|
|
|
|
|
|
|
|
|
### La couverture des tests
|
|
|
|
|
|
|
|
|
|
📖 On appelle la *couverture des tests* (test coverage), la partie de code pour laquelle il existe des tests.
|
|
|
|
|
|
|
|
|
|
⚠ Il est impossible de tester tous les cas possibles et d'avoir une couverture parfaites. Il y a donc un compromis à faire sur l'utilité des tests et le temps pour les écrire.
|
|
|
|
|
|
|
|
|
|
Par exemple, sur un code non-collaboratif, stable, simple, sans nécessité de garanties, les tests se justifieront moins sur un code collaboratif, instable, complexe et avec obligations contractuelles.
|
|
|
|
|
|
|
|
|
|
⚠ Il ne faut pas aussi que le test soit redondant avec l'implémentation, i.e. utilise le code de la fonction pour vérifier son résultat :
|
|
|
|
|
|
|
|
|
|
```ts
|
|
|
|
|
function add(a,b) { return a+b; }
|
|
|
|
|
|
|
|
|
|
Deno.test('Addition', async() => {
|
|
|
|
|
const answer = add(1,2);
|
|
|
|
|
if( answer !== (1+2) ) // Vérifie que 1+2 === 1+2...
|
|
|
|
|
throw new Error(`\nExpected: ${1+2}\nGot: ${answer}`);
|
|
|
|
|
});
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
On peut distinguer 4 contextes dans l'écriture de code :
|
|
|
|
|
|
|
|
|
|
- *le comportement attendu est connu, mais pas l'algorithme* : on connait les données en entrées, les données en sortie, mais on ne sait pas encore la manière dont on va l'implémenter. Écrire des tests permet alors de décrire le comportement attendu, ainsi que de vérifier que l'implémentation respecte ces attendus.
|
|
|
|
|
|
|
|
|
|
*e.g. compter le nombre de mots dans une phrase.*
|
|
|
|
|
|
|
|
|
|
- *re-implémente un algorithme* (e.g. dans un autre langage, version plus rapide, etc.) : on peut alors tester les résultats de notre nouvelle implémentation contre les implémentations existantes, les mêmes entrées devant produire les mêmes résultats.
|
|
|
|
|
*e.g. signature RSA.*
|
|
|
|
|
|
|
|
|
|
- *l'algorithme est connu, mais pas les résultats* : il est nécessaire d'exécuter l'algorithme pour connaître le résultat. Dans ce cadre les tests ont de grandes chances d'être redondants avec l'implémentation. Toutefois des tests de non-régressions peuvent être implémentés en pré-calculant les résultats attendus.
|
|
|
|
|
|
|
|
|
|
*e.g. la somme des éléments d'une liste.*
|
|
|
|
|
|
|
|
|
|
- *l'algorithme est une traduction du comportement attendu* : l'implémentation a de grande chance d'être triviale, et les tests d'être redondants.
|
|
|
|
|
|
|
|
|
|
*e.g. la liste doit être vide à l'initialisation.*
|
|
|
|
|
|
|
|
|
|
### Les tests pour déboguer
|
|
|
|
|
|
|
|
|
|
Il est en effet très intéressant d'utiliser des tests pour déboguer. Lorsqu'un bug est constaté, il faut pouvoir le reproduire, généralement via un exemple minimal reproduisant l'erreur. Il ne reste alors plus qu'à ajouter une vérification pour obtenir un test :
|
|
|
|
|
|
|
|
|
|
```ts
|
|
|
|
|
// Exemple minimal :
|
|
|
|
|
let answer = authenticate("h4cker", "**********")
|
|
|
|
|
// expected : false
|
|
|
|
|
// got : true
|
|
|
|
|
|
|
|
|
|
// Le test (Deno) :
|
|
|
|
|
// Git issue #XXXX
|
|
|
|
|
Deno.test('H4cker authentication #XXXX', async() => {
|
|
|
|
|
let answer = authenticate("h4cker", "**********");
|
|
|
|
|
if( answer !== false)
|
|
|
|
|
throw new Error(`\nExpected: ${false}\nGot: ${answer}`);
|
|
|
|
|
})
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Le bug sera alors considéré résolu lorsqu'il passera le test. Aussi, contrairement à des `console.log()` qui sont temporaires, les tests restent, et pourront être réexécutés à chaque *pull request*, afin de prévenir les régressions.
|
|
|
|
|
|
|
|
|
|
> « Les logs s'envolent, les tests restent »
|
|
|
|
|
|
|
|
|
|
### Tester le serveur (Deno)
|
|
|
|
|
|
|
|
|
|
Deno offre une API permettant aisément d'écrire et exécuter des suites de tests (*tests suite*) :
|
|
|
|
|
|
|
|
|
|
```typescript
|
|
|
|
|
Deno.test($TEST_NAME, async() => {
|
|
|
|
|
//...
|
|
|
|
|
if( $COND )
|
|
|
|
|
throw new Error($MSG);
|
|
|
|
|
});
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
```ts
|
|
|
|
|
deno test $FILE # exécuter le fichier de test $FILE
|
|
|
|
|
deno test $DIR # exécuter les tests contenu dans le dossier $DIR.
|
|
|
|
|
deno test # exécuter tous les tests du projet.
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
💡 La commande `deno run` supporte aussi les options `--watch` et `--check`.
|
|
|
|
|
|
|
|
|
|
💡 Vous pouvez écrire vos propres fonctions d'assertions, ou utiliser [une bibliothèque dédiée](https://docs.deno.com/runtime/manual/basics/testing/assertions) :
|
|
|
|
|
|
|
|
|
|
```ts
|
|
|
|
|
function assertEquals(got: any, expected: any, msg: string = '') {
|
|
|
|
|
if( got !== expected ) throw new Error
|
|
|
|
|
(`${msg}
|
|
|
|
|
\x1b[1;31m- ${got}\x1b[0m
|
|
|
|
|
\x1b[1;32m+ ${expected}\x1b[0m`);
|
|
|
|
|
}
|
|
|
|
|
// ou
|
|
|
|
|
import * from "https://deno.land/std@0.215.0/assert/mod.ts";
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Deno.test('H4cker authentication #XXXX', async () => {
|
|
|
|
|
let answer = authenticate("h4cker", "**********");
|
|
|
|
|
assertEquals(answer, false, 'cf issue #XXXX');
|
|
|
|
|
});
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
💡 Si vous avez beaucoup de tests, vous pouvez définir des sous-tests :
|
|
|
|
|
|
|
|
|
|
```ts
|
|
|
|
|
Deno.test('Test', async (t) => {
|
|
|
|
|
await t.step('Sous-test', async(t) => {
|
|
|
|
|
await t.step('Sous-sous-test', async() => {});
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
💡 Vous pouvez aussi réinitialiser les valeurs initiales avant chaque sous-tests :
|
|
|
|
|
|
|
|
|
|
```ts
|
|
|
|
|
Deno.test('Test', async (t) => {
|
|
|
|
|
|
|
|
|
|
let user!: User;
|
|
|
|
|
|
|
|
|
|
async function step(name: string, callback: ()=>Promise<void>) {
|
|
|
|
|
user = new User();
|
|
|
|
|
await t.step(name, callack);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
await step('Sous-test', async() => {
|
|
|
|
|
//...
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### Tester le client
|
|
|
|
|
|
|
|
|
|
Vous pouvez très simplement tester des bouts de code du client en les important puis exécutant sur Deno. Pour simuler le DOM, vous pouvez utiliser la bibliothèque `DOMParser` qui vous permettra ensuite d'interagir avec :
|
|
|
|
|
|
|
|
|
|
```ts
|
|
|
|
|
import { DOMParser } from "https://deno.land/x/deno_dom/deno-dom-wasm.ts";
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const document = new DOMParser().parseFromString( await Deno.readFile(...) )
|
|
|
|
|
const element = document.querySelector('#elem');
|
|
|
|
|
element.click();
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Une autre solution est de lancer le site Web dans un navigateur en lui injectant du code JavaScript afin de le manipuler à partir d'un serveur. C'est par exemple ce que fait [Puppeteer](https://deno.land/x/puppeteer@16.2.0) :
|
|
|
|
|
|
|
|
|
|
```ts
|
|
|
|
|
import puppeteer from "https://deno.land/x/puppeteer@16.2.0/mod.ts";
|
|
|
|
|
|
|
|
|
|
// démarrage.
|
|
|
|
|
const browser = await puppeteer.launch();
|
|
|
|
|
const page = await browser.newPage();
|
|
|
|
|
await page.goto("https://example.com");
|
|
|
|
|
|
|
|
|
|
// exécuter du code sur le navigateur :
|
|
|
|
|
const result = await page.evaluate(() => { ... });
|
|
|
|
|
// 💡 vous pouvez aussi transmettre des valeurs :
|
|
|
|
|
const result = await page.evaluate((id, name) => { ... }, 1, 'foo');
|
|
|
|
|
|
|
|
|
|
// prendre une capture d'écran
|
|
|
|
|
await page.screenshot({ path: "example.png" });
|
|
|
|
|
|
|
|
|
|
// fin des tests.
|
|
|
|
|
await browser.close();
|
|
|
|
|
```
|