@starterlib/testing
v0.16.5
Published
Outils de test du socle frontal starter : harnais, fixtures, simulateurs, hote de developpement des remotes. Jamais en dependance de production.
Maintainers
Readme
@starterlib/testing
Outils de test du socle frontal starter. Jamais en dependance de production.
Installation
pnpm add -D @starterlib/testing axe-coreQuel pair sert quel point d'entree
| Point d'entree | Pairs importes a l'execution | Requis ou optionnel |
| -------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------- |
| @starterlib/testing | axe-core | requis |
| @starterlib/testing/ui | @angular/core, @starterlib/ui | optionnels : a installer pour ce point seul |
| @starterlib/testing/hote-dev | @angular/core, common/http, router, platform-browser, rxjs, @starterlib/core, @starterlib/ui (tronc, navigation, libelles-en) | optionnels : ceux qu'un remote a deja |
| @starterlib/testing/assemblage | @playwright/test ; @axe-core/playwright a l'appel de manquementsDAccessibilite | optionnels : ceux des essais de navigateur |
Un projet qui teste sans Angular (des utilitaires, des jetons) n'installe que axe-core. Un projet
qui monte un ecran du socle avec socleDeTest installe aussi @angular/core et @starterlib/ui,
qu'il a deja. Un remote qui se lance seul (pnpm start) avec @starterlib/testing/hote-dev a deja
tous les pairs de ce point d'entree ; il eprouve ce pnpm start en vrai navigateur avec
@starterlib/testing/assemblage, s'il installe @playwright/test. La table est gardee par tools/verifier-paquets.mjs (DEPENDANCES_ADMISES, lot 153) :
le tronc qui importerait Angular, ou testing/ui un autre pair, fait echouer pnpm paquets:check.
Contenu
verifierAccessibilite(container) : passe axe-core sur un fragment rendu et
echoue en nommant chaque violation, sa regle et l'element fautif.
import { verifierAccessibilite } from '@starterlib/testing';
it("ne produit aucune violation d'accessibilite", async () => {
const { container } = await render(MonComposant);
await verifierAccessibilite(container);
});⚠️ Sous jsdom, axe ne voit ni la mise en page ni la cascade CSS : le contraste,
la taille des cibles et la visibilite reelle ne sont pas couverts. Ces controles
demandent un vrai navigateur : le depot les mesure sur son banc, en Chromium
(pnpm test:navigateur, voir le README de @starterlib/ui), et ce paquet n'en
exporte que la mesure d'un sprite (mesurerUnSprite, plus bas).
REGLES_DESACTIVEES_PAR_DEFAUT nomme les regles d'axe que la verification
desactive d'office, chacune avec sa raison (color-contrast, que jsdom ne peut
pas calculer), pour qu'un test puisse les afficher.
Une regle se desactive en plus par exemptions, avec sa raison ECRITE : quarante
caracteres au moins (LONGUEUR_MINIMALE_D_UNE_RAISON, 0.10.2), sans quoi la
verification leve. « faux positif » ou une chaine vide ne se relisent pas.
gravitesBloquantes choisit les niveaux qui font echouer (toutes par defaut) :
await verifierAccessibilite(container, {
exemptions: {
'button-name': "Le nom est pose par la directive hote, absente de ce montage d'essai.",
},
gravitesBloquantes: ['serious', 'critical'],
});amorcerJsdom() : comble ce que jsdom ne fournit pas — scrollIntoView
(absent) et le defilement de la fenetre (present mais inerte, il journalise
« Not implemented »). A poser une fois, en setupFiles :
// angular.json, cible `test` : "setupFiles": ["tools/amorce-de-test.ts"]
import { amorcerJsdom } from '@starterlib/testing';
amorcerJsdom();elementsFocalisables(racine) : ce que la tabulation traverse, dans l'ordre du
DOM. Les sentinelles d'un piege a focus du CDK (cdk-focus-trap-anchor) n'y sont
pas : le focus qui en atteint une est aussitot renvoye dans le piege (0.10.2).
annonces(racine) et attendreLAnnonce(racine, texte) : ce que disent les
regions vivantes, sans attendre au hasard — l'echec dit ce qui a ete lu.
attendreLAnnonce guette les MUTATIONS du fragment, pas l'horloge (0.10.2) : elle
marche sous minuteries reelles comme sous vi.useFakeTimers(). Sous de faux
minuteurs, l'annonce qu'un minuteur pose arrive des que l'essai avance le temps,
et l'echeance tombe quand le temps simule la depasse :
vi.useFakeTimers();
const annonce = attendreLAnnonce(document.body, 'Page 2');
await vi.advanceTimersByTimeAsync(delai.annonce);
await expect(annonce).resolves.toBe('Page 2');exercerLesLibelles(catalogue, options) : lit CHAQUE entree d'un dictionnaire —
une chaine telle quelle, une fonction appelee avec des arguments d'exemple, un
sous-dictionnaire en profondeur — et rend les textes ({ cle, texte }). Une
traduction qu'aucun essai n'appelle n'a jamais ete lue : une interpolation
oubliee n'apparaissait qu'a l'ecran. Une entree a domaine ferme (un etat) recoit
ses arguments representatifs par exemples :
for (const { cle, texte } of exercerLesLibelles(LIBELLES_EN, {
exemples: { etatDeDocument: ['analyse'] },
})) {
expect(texte.trim(), cle).not.toBe('');
expect(texte, cle).not.toMatch(/undefined|NaN/);
}poserMouvementReduit() : pose data-mouvement="reduit" sur <html>, comme
StMouvement pour une personne qui l'a choisi, et rend la fonction qui REMET la
valeur precedente (et non qui retire l'attribut). C'est la seule voie que jsdom
sache poser ; la requete de media de la feuille de jetons
(prefers-reduced-motion) se prouve en vrai navigateur.
simulerUneRequeteDeMedia(requete, correspond) : remplace matchMedia par une
requete que l'on fait basculer (basculer), qui compte ses ecouteurs change
(ecouteurs) et rend l'original (restaurer) ; toute autre requete repond « ne
correspond pas ». Un service qui SUIT le systeme (StTheme sur
prefers-color-scheme) se prouve sur un changement, et par le retrait de son
ecouteur a la destruction (constat A1063).
@starterlib/testing/ui
import { socleDeTest } from '@starterlib/testing/ui';
const socle = socleDeTest({ locale: 'fr-FR' });
await render(MonEcran, { providers: socle.fournisseurs });
socle.locale.set('en-GB'); // les formats doivent suivre, sans rechargement⚠️ Point d'entree separe parce qu'il importe @starterlib/ui : laisse dans le
tronc, il ferait compiler tout le paquet de composants dans les tests d'un paquet
qui ne s'en sert pas.
analyserUnSprite(texte) et mesurerUnSprite(source, options) : les regles de
pnpm icones:check, pour qu'un remote mesure SON sprite metier au lieu de le
croire. analyserUnSprite ne lit que le TEXTE (un <symbol> par icone, en
fill="currentColor" sur une grille carree, des formes simples, aucun script,
style, transform ni couleur litterale) et tourne partout. mesurerUnSprite
ajoute la parite avec les noms attendus (noms), une encre ni hors de la grille
de 24 ni perdue dans son cadre, et l'equilibre d'une serie de statut
(serieDeStatut) : elle exige un VRAI moteur de rendu (Vitest navigateur,
Playwright), et refuse sous jsdom au lieu de rendre des boites nulles.
import { mesurerUnSprite } from '@starterlib/testing/ui';
const rapport = await mesurerUnSprite('/assets/icones/sprite.svg', { noms: ['colis'] });
expect(rapport.defauts).toEqual([]);⚠️ Exception documentee a « aucun acces reseau hors de @starterlib/core »
(docs/plan-librairie.md) : passee une ADRESSE, mesurerUnSprite lit le sprite
par un fetch nu, sans les intercepteurs du noyau — ni session, ni jeton, ni
correlation. Admise parce que ce paquet ne part jamais en production : l'outil
lit l'actif que sert le serveur de test. Un texte SVG (qui commence par <) est
lu tel quel, sans requete.
A venir
Simulateurs de reseau et de session. Le banc en vrai navigateur, lui, existe
depuis la 0.6.0 : il vit dans le depot (pnpm test:navigateur) et n'est pas
publie.
L'hote de developpement d'un remote (@starterlib/testing/hote-dev, lot 269)
Ce que pnpm start sert, sans BFF ni coquille : les MEMES fournisseurs que la coquille (session,
langue, navigation, jetons, sprite), une coquille dessinee (gabarit courant, ou pleine page pour un
remote pleinePage), la navigation tiree du manifeste source, chaque route montee sous SON prefixe,
la racine renvoyee a la premiere, et toute autre adresse nommee (session simulee comprise) au lieu
d'une page blanche.
// src/main.ts : la carte d'importation d'abord (les paquets partages s'y resolvent), puis l'hote.
import { initFederation } from '@angular-architects/native-federation';
initFederation()
.then(() => import('./bootstrap'))
.then((m) => m.demarrer())
.catch((erreur: unknown) => {
console.error("l'hote de developpement n'a pas pu demarrer", erreur);
});
// src/bootstrap.ts
import { demarrerLHoteDeDeveloppement } from '@starterlib/testing/hote-dev';
import manifeste from '../manifest.source.json' with { type: 'json' };
import simulations from './hote-dev/simulations.engendre.json' with { type: 'json' };
import { CHEMIN_DU_SPRITE_METIER } from './app/icones/sprite-genere';
export const demarrer = () =>
demarrerLHoteDeDeveloppement({
manifeste,
modules: { './routes': () => import('./app/routes').then((m) => m.routes) },
stubs: simulations.stubs,
spriteMetier: CHEMIN_DU_SPRITE_METIER,
});Le backend est simule DANS le navigateur (BackendSimule), dans cet ordre : les simulations ecrites
du remote (simulations, un etat ou un calcul), les stubs WireMock du service
(starter-web simulations les recopie de ../src/mock, corps compris : une seule simulation, celle
du profil mock du BFF), le catalogue socle de l'hote, puis le BFF (GET /bff/session, alice
ouverte par defaut ; POST /bff/logout). Le reste (catalogues, sprites) va au serveur de
developpement. Chaque reponse attend LATENCE_SIMULEE (250 ms), erreurs comprises : un essai la
pose a 0.
Les etats vide et en erreur d'un ecran se regardent sans ecrire de simulation, par l'adresse de la
page (lot 274) : ?simuler=vide rend chaque liste lue a l'API videe (un tableau [], une page par
curseur vide et derniere ; une fiche passe telle quelle, une ecriture aussi), ?simuler=erreur fait
repondre chaque appel d'API par une panne 503 PLATFORM_DEPENDENCY_UNAVAILABLE. Seuls les appels
/api/... en changent : la session, les catalogues et les actifs restent servis. L'hote le dit en
banniere ; retirer le parametre rend les simulations. Un essai remplace ETAT_SIMULE.
Chaque etat d'une page par son adresse (P08-10)
Pour qu'un ecran se valide page par page sans toucher au code, l'adresse de la page demande aussi :
| Parametre | Ce que l'hote fait |
| -------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| ?simuler=lent | chaque appel d'API attend 3 s (LATENCE_LENTE_PAR_DEFAUT), ou ?latence=<ms> (60 s au plus), celui que le serveur de developpement sert compris ; le BFF et les catalogues gardent leur latence |
| ?simuler=hors-ligne | la page s'ouvre (catalogues, session), puis le reseau tombe (StCoupureSimulee) : chaque requete echoue sans atteindre le serveur (statut 0), la sonde fetch de StConnectivite aussi, et elle dit hors ligne |
| ?scenario=<nom> | un scenario nomme du remote passe AVANT tout le reste (ses simulations ecrites, puis ses stubs), et sa session s'il en change une ; seul, ou avec ?simuler=lent ou ?simuler=hors-ligne |
| ?langue=fr ou en | la langue de l'interface au demarrage |
| ?theme=clair ou sombre | le theme au demarrage |
| ?horloge=<instant ISO> | l'horloge de la page part de cet instant, puis avance (reglerLHorloge) : echeances et decomptes s'ouvrent au meme point a chaque chargement |
⚠️ L'horloge est reglee, jamais arretee : arretee, elle bloquerait ce qui mesure le temps qui
passe (debounceTime de RxJS reprogramme sans fin, l'avertissement d'inactivite ne viendrait
plus). Elle ne vit que dans la page de l'hote : demarrerLHoteDeDeveloppement la regle avant le
demarrage, et le livrable de production ne porte pas l'hote.
Les scenarios se declarent dans le fichier de scenarios du remote, a cote des simulations
engendrees ; starter-web simulations --scenarios les liste avec leur adresse, et l'etape
simulations de starter-web verify les juge. Le modele :
{
"scenarios": {
"liste-vide": {
"description": "Aucun element : l'etat vide de la liste",
"stubs": [
{
"request": { "method": "GET", "urlPath": "/api/v1/elements" },
"response": { "status": 200, "jsonBody": { "content": [], "nextCursor": null } }
}
]
},
"gestionnaire": {
"description": "Session d'un gestionnaire",
"session": { "nomAffiche": "Gabriel Essai", "roles": ["GESTIONNAIRE"] }
}
}
}// src/bootstrap.ts : le fichier passe a l'hote, a cote des stubs.
import scenarios from './hote-dev/scenarios.json' with { type: 'json' };
export const demarrer = () =>
demarrerLHoteDeDeveloppement({
manifeste,
modules,
stubs: simulations.stubs,
scenarios: scenarios.scenarios,
});Un scenario porte une description (dite dans la banniere), des stubs (le sous-ensemble de
WireMock ci-dessous, corps EN LIGNE : jsonBody ou body) et une session (ce qu'elle change de
la session simulee). En TypeScript, simulations y ajoute un calcul. Noms et donnees fictifs :
aucune donnee personnelle reelle. La banniere de l'hote liste les scenarios, chacun par un lien qui
recharge la page courante avec son nom (les autres parametres gardes), dit un scenario inconnu,
l'etat simule et l'horloge reglee ; elle se masque.
Les stubs se rejouent dans un sous-ensemble de WireMock : method, url, urlPattern, urlPath,
urlPathPattern, queryParameters et headers (equalTo, contains, matches, absent),
bodyPatterns (equalTo, contains, matches, equalToJson, matchesJsonPath : $.a.b,
['a'], [0], [*], filtres [?(@.a == 'x')]), priority ; en reponse status, headers,
jsonBody ou body, fixedDelayMilliseconds. L'en-tete Authorization exige est ignore (le BFF
le poserait). Un stub hors de ce sous-ensemble est ecarte, et nomme.
⚠️ Jamais en production : la configuration production d'un remote remplace src/main.ts par un
point d'entree vide, et l'etape securite de starter-web verify refuse un livrable qui porterait
l'hote.
Les essais d'assemblage en vrai navigateur (@starterlib/testing/assemblage, lot 269)
Un point d'entree de NODE, pour le lanceur Playwright : il n'est jamais charge par une page. Il porte les outils generiques du banc d'assemblage du BFF, et ceux qu'un remote emploie pour eprouver son hote de developpement :
// e2e/playwright.config.ts du remote
import { defineConfig, devices } from '@playwright/test';
import { serveurDeLHoteDeDeveloppement } from '@starterlib/testing/assemblage';
export default defineConfig({
testDir: '.',
testMatch: '**/*.e2e.ts',
webServer: serveurDeLHoteDeDeveloppement({ port: 4302 }),
use: { baseURL: 'http://localhost:4302', locale: 'fr-FR' },
projects: [{ name: 'chromium', use: { ...devices['Desktop Chrome'] } }],
});// e2e/hote-dev.e2e.ts
import { expect, test } from '@playwright/test';
import {
manquementsDAccessibilite,
policeDessinee,
surveillerLaConsole,
surveillerLesViolations,
} from '@starterlib/testing/assemblage';
test('la racine mene a la route du manifeste, dans la police du socle', async ({ page }) => {
const erreurs = surveillerLaConsole(page);
const violations = await surveillerLesViolations(page);
await page.goto('/');
await expect(page).toHaveURL(/\/catalogue$/);
expect((await policeDessinee(page, 'h1')).chargeeParLaPage).toBe(true);
expect(await manquementsDAccessibilite(page)).toEqual([]);
expect(await violations()).toEqual([]);
expect(erreurs()).toEqual([]);
});| Outil | Ce qu'il dit |
| --------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| serveurDeLHoteDeDeveloppement({ port, delai, reutiliser, dossier }) | le webServer : starter-web start --port <port> par Node, jamais pnpm start (0.10.7) ; attend remoteEntry.json, pas la racine (page blanche avant la construction federee) ; 4 min |
| surveillerLesViolations(page) | les violations de la politique de securite du contenu, « directive adresse », la politique Trusted Types refusee nommee ; a appeler avant le premier chargement |
| surveillerLaConsole(page) | les erreurs de console et les exceptions non rattrapees ; pas les avertissements |
| champ(page, libelle, { obligatoire, mention }) | un champ par son nom accessible COMPLET, mention d'obligation comprise (MENTION_OBLIGATOIRE_FR par defaut, MENTION_OBLIGATOIRE_EN) ; egalite stricte |
| attendreLeTitre(page, titre) | le titre de niveau 1, attendu |
| manquementsDAccessibilite(page) | axe sous ETIQUETTES_WCAG_AA (WCAG 2.0 a 2.2, A et AA), une ligne par regle, sur la page AU REPOS : animations et transitions en cours attendues (5 s au plus), sauf sans fin ou en pause |
| commandesTropPetites(page) | les boutons et champs sous 24 x 24 px ; l'etiquette d'un champ masque est la cible mesuree |
| deborde(page) | vrai quand la page defile horizontalement |
| regionsTraversees(page, n) | les regions que la tabulation traverse, doublons consecutifs reduits |
| policeDessinee(page, selecteur) | les polices qui PEIGNENT le texte direct de l'element (Chromium : protocole du navigateur), et si la page les a chargees ; getComputedStyle ne dit que la pile demandee |
Ce point d'entree n'a ni serveur statique a nonce, ni doubles partages de StSession, StLangue
et StTheme, par decision (lot 269) : l'assemblage sous la vraie politique du BFF est
pnpm start:federe (le BFF en profil mock, lot 285), qu'une copie de la politique ferait deriver ;
l'hote de developpement pose deja les VRAIS services sur un backend simule, et un essai d'ecran
garde des doubles dont il decide les reponses.
Les outils s'eprouvent en Chromium par pnpm test:assemblage (pages servies par interception, sous
une politique reelle), que pnpm verify:navigateur joue.
