Ajouter validation automatisee Playwright
This commit is contained in:
parent
4538d04972
commit
ffa70fcc04
11 changed files with 1521 additions and 3 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -1,6 +1,9 @@
|
||||||
node_modules/
|
node_modules/
|
||||||
dist/
|
dist/
|
||||||
__pycache__/
|
__pycache__/
|
||||||
|
.tmp/
|
||||||
|
frontend/test-results/
|
||||||
|
frontend/playwright-report/
|
||||||
*.py[cod]
|
*.py[cod]
|
||||||
*.retry
|
*.retry
|
||||||
ansible/group_vars/vault.yml
|
ansible/group_vars/vault.yml
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,8 @@ Consignes pour les agents IA qui modifient ce dépôt. Ce fichier est une bousso
|
||||||
- Exploitation: `docs/90_EXPLOITATION.md`
|
- Exploitation: `docs/90_EXPLOITATION.md`
|
||||||
- Déploiement Ansible: `docs/91_DEPLOIEMENT_ANSIBLE.md`
|
- Déploiement Ansible: `docs/91_DEPLOIEMENT_ANSIBLE.md`
|
||||||
- Multi-groupe: `docs/93_MULTI_GROUPE.md`
|
- Multi-groupe: `docs/93_MULTI_GROUPE.md`
|
||||||
|
- Plan de validation: `docs/96_PLAN_VALIDATION.md`
|
||||||
|
- Validation automatisée: `docs/97_VALIDATION_AUTOMATISEE.md`
|
||||||
- Garde-fous sociaux: `docs/94_GARDE_FOUS_SOCIAUX.md`
|
- Garde-fous sociaux: `docs/94_GARDE_FOUS_SOCIAUX.md`
|
||||||
|
|
||||||
Lire les documents pertinents avant un changement large. Ne pas recopier leur contenu ici.
|
Lire les documents pertinents avant un changement large. Ne pas recopier leur contenu ici.
|
||||||
|
|
@ -79,9 +81,11 @@ Chaque groupe a un `tenant_schema` PostgreSQL. La voie applicative principale ut
|
||||||
Choisir les validations selon le changement:
|
Choisir les validations selon le changement:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
scripts/validate.sh
|
||||||
python3 -m py_compile <fichiers_python>
|
python3 -m py_compile <fichiers_python>
|
||||||
.venv/bin/python -m pytest backend/tests/test_business_rules.py
|
.venv/bin/python -m pytest backend/tests
|
||||||
npm run build
|
npm run build
|
||||||
|
(cd frontend && npm run e2e)
|
||||||
ansible-playbook ansible/site.yml --syntax-check
|
ansible-playbook ansible/site.yml --syntax-check
|
||||||
ansible-playbook ansible/verify.yml --syntax-check
|
ansible-playbook ansible/verify.yml --syntax-check
|
||||||
ansible-playbook ansible/backup-now.yml --syntax-check
|
ansible-playbook ansible/backup-now.yml --syntax-check
|
||||||
|
|
|
||||||
108
backend/tests/test_validation_contracts.py
Normal file
108
backend/tests/test_validation_contracts.py
Normal file
|
|
@ -0,0 +1,108 @@
|
||||||
|
import unittest
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[2]
|
||||||
|
|
||||||
|
|
||||||
|
def read(rel: str) -> str:
|
||||||
|
return (ROOT / rel).read_text(encoding="utf-8")
|
||||||
|
|
||||||
|
|
||||||
|
class ValidationPlanTests(unittest.TestCase):
|
||||||
|
def test_validation_plan_is_indexed_and_structured(self):
|
||||||
|
index = read("docs/00_INDEX.md")
|
||||||
|
plan = read("docs/96_PLAN_VALIDATION.md")
|
||||||
|
|
||||||
|
self.assertIn("96_PLAN_VALIDATION.md", index)
|
||||||
|
self.assertGreaterEqual(plan.count("Scénario:"), 40)
|
||||||
|
self.assertEqual(plan.count("Scénario:"), plan.count("Résultats attendus:"))
|
||||||
|
|
||||||
|
def test_validation_plan_covers_all_critical_domains(self):
|
||||||
|
plan = read("docs/96_PLAN_VALIDATION.md")
|
||||||
|
for section in [
|
||||||
|
"Accès, groupes et identité",
|
||||||
|
"Navigation et ergonomie",
|
||||||
|
"Membres",
|
||||||
|
"Postes, mandats et permissions",
|
||||||
|
"Rencontres",
|
||||||
|
"Assemblées",
|
||||||
|
"Gouvernance",
|
||||||
|
"Contributions",
|
||||||
|
"Dépenses",
|
||||||
|
"Trésorerie",
|
||||||
|
"Inventaires, littérature et jetons",
|
||||||
|
"Événements",
|
||||||
|
"Rapports PDF et mode papier",
|
||||||
|
"Journal, historique et notifications",
|
||||||
|
"Sauvegarde, restauration et exploitation",
|
||||||
|
"Concurrence et changements simultanés",
|
||||||
|
]:
|
||||||
|
with self.subTest(section=section):
|
||||||
|
self.assertIn(f"## {section}", plan)
|
||||||
|
|
||||||
|
|
||||||
|
class PublicLandingContractTests(unittest.TestCase):
|
||||||
|
def test_public_landing_contains_only_public_links_and_sysadmin_access(self):
|
||||||
|
login = read("frontend/src/pages/Login.jsx")
|
||||||
|
|
||||||
|
self.assertIn("Choisir un groupe", login)
|
||||||
|
self.assertIn("Sysadmin", login)
|
||||||
|
self.assertIn("Charte", login)
|
||||||
|
self.assertIn("Confidentialité", login)
|
||||||
|
self.assertIn("Découvrir", login)
|
||||||
|
self.assertIn("setPage('G14')", login)
|
||||||
|
self.assertIn("localStorage.removeItem('gm_groupe_id')", login)
|
||||||
|
self.assertIn('type="password"', login)
|
||||||
|
|
||||||
|
def test_group_home_does_not_expose_instance_admin_or_public_policy_links(self):
|
||||||
|
accueil = read("frontend/src/pages/Accueil.jsx")
|
||||||
|
|
||||||
|
for forbidden in ["setPage('G14')", "Charte", "Confidentialité", "Découvrir"]:
|
||||||
|
with self.subTest(forbidden=forbidden):
|
||||||
|
self.assertNotIn(forbidden, accueil)
|
||||||
|
|
||||||
|
|
||||||
|
class SafetyContractTests(unittest.TestCase):
|
||||||
|
def test_api_client_sends_resource_version_for_concurrency_protection(self):
|
||||||
|
api = read("frontend/src/stores/api.js")
|
||||||
|
|
||||||
|
self.assertIn("options.version", api)
|
||||||
|
self.assertIn("X-Resource-Version", api)
|
||||||
|
|
||||||
|
def test_crystallized_reports_guard_meeting_inputs(self):
|
||||||
|
guarded_files = [
|
||||||
|
"backend/app/routers/reunions.py",
|
||||||
|
"backend/app/routers/presences.py",
|
||||||
|
"backend/app/routers/pv.py",
|
||||||
|
"backend/app/routers/ventes_litterature.py",
|
||||||
|
"backend/app/routers/jetons.py",
|
||||||
|
]
|
||||||
|
for rel in guarded_files:
|
||||||
|
source = read(rel)
|
||||||
|
with self.subTest(router=rel):
|
||||||
|
self.assertIn("verifier_reunion_non_cristallisee", source)
|
||||||
|
|
||||||
|
def test_group_backup_and_restore_are_group_scoped_operations(self):
|
||||||
|
admin_router = read("backend/app/routers/admin.py")
|
||||||
|
gestion_groupes = read("frontend/src/pages/GestionGroupes.jsx")
|
||||||
|
|
||||||
|
self.assertIn("/groupes/{groupe_id}/backup", admin_router)
|
||||||
|
self.assertIn("/groupes/{groupe_id}/restore", admin_router)
|
||||||
|
self.assertIn("creer_sauvegarde_groupe", admin_router)
|
||||||
|
self.assertIn("restaurer_sauvegarde_groupe", admin_router)
|
||||||
|
self.assertIn("Réinit", gestion_groupes)
|
||||||
|
self.assertIn("Restaurer", gestion_groupes)
|
||||||
|
|
||||||
|
def test_instance_admin_token_is_kept_out_of_group_context(self):
|
||||||
|
admin_router = read("backend/app/routers/admin.py")
|
||||||
|
security = read("backend/app/core/security.py")
|
||||||
|
|
||||||
|
self.assertIn("create_token(str(admin.id), None", admin_router)
|
||||||
|
self.assertIn('subject_type="instance_admin"', admin_router)
|
||||||
|
self.assertIn('payload.get("typ") == "instance_admin"', security)
|
||||||
|
self.assertIn("return admin", security)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
|
|
@ -23,6 +23,8 @@ Pour administrer ou exploiter l'application:
|
||||||
- [Déploiement](92_DEPLOIEMENT.md)
|
- [Déploiement](92_DEPLOIEMENT.md)
|
||||||
- [Sécurité et confidentialité](80_SECURITE.md)
|
- [Sécurité et confidentialité](80_SECURITE.md)
|
||||||
- [Multi-groupe](93_MULTI_GROUPE.md)
|
- [Multi-groupe](93_MULTI_GROUPE.md)
|
||||||
|
- [Plan de validation fonctionnelle](96_PLAN_VALIDATION.md)
|
||||||
|
- [Validation automatisée](97_VALIDATION_AUTOMATISEE.md)
|
||||||
|
|
||||||
Pour maintenir ou faire évoluer le code:
|
Pour maintenir ou faire évoluer le code:
|
||||||
|
|
||||||
|
|
@ -31,6 +33,8 @@ Pour maintenir ou faire évoluer le code:
|
||||||
- [API applicative](70_API.md)
|
- [API applicative](70_API.md)
|
||||||
- [Règles métier](30_REGLES_METIER.md)
|
- [Règles métier](30_REGLES_METIER.md)
|
||||||
- [Liens de causalité](40_LIENS_CAUSALITE.md)
|
- [Liens de causalité](40_LIENS_CAUSALITE.md)
|
||||||
|
- [Plan de validation fonctionnelle](96_PLAN_VALIDATION.md)
|
||||||
|
- [Validation automatisée](97_VALIDATION_AUTOMATISEE.md)
|
||||||
|
|
||||||
## Organisation fonctionnelle
|
## Organisation fonctionnelle
|
||||||
|
|
||||||
|
|
|
||||||
1008
docs/96_PLAN_VALIDATION.md
Normal file
1008
docs/96_PLAN_VALIDATION.md
Normal file
File diff suppressed because it is too large
Load diff
96
docs/97_VALIDATION_AUTOMATISEE.md
Normal file
96
docs/97_VALIDATION_AUTOMATISEE.md
Normal file
|
|
@ -0,0 +1,96 @@
|
||||||
|
# Validation automatisée
|
||||||
|
|
||||||
|
Statut: runbook de validation
|
||||||
|
Public: testeurs, mainteneurs, sysadmin
|
||||||
|
Dernière révision: 2026-06-02
|
||||||
|
|
||||||
|
Ce document explique comment automatiser la validation sans remplacer le jugement humain nécessaire pour l'acceptabilité, la lisibilité et l'ergonomie.
|
||||||
|
|
||||||
|
## Commande locale
|
||||||
|
|
||||||
|
Depuis la racine du dépôt:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
scripts/validate.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Cette commande exécute:
|
||||||
|
|
||||||
|
- compilation Python du backend et des scripts de déploiement;
|
||||||
|
- tests backend;
|
||||||
|
- build frontend;
|
||||||
|
- tests navigateur Playwright;
|
||||||
|
- syntax-check Ansible lorsque `ansible-playbook` est disponible.
|
||||||
|
|
||||||
|
Pour ignorer temporairement les tests navigateur:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
SKIP_E2E=1 scripts/validate.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
## Ce qui est automatisé
|
||||||
|
|
||||||
|
- règles métier testables par code;
|
||||||
|
- contrats de sécurité et de multi-groupe;
|
||||||
|
- présence des protections de concurrence;
|
||||||
|
- cristallisation des rapports;
|
||||||
|
- exposition correcte de l'accès sysadmin;
|
||||||
|
- page publique de sélection des groupes;
|
||||||
|
- accès sysadmin d'instance;
|
||||||
|
- changement du PIN sysadmin;
|
||||||
|
- liens publics Charte, Confidentialité et Découvrir;
|
||||||
|
- build frontend;
|
||||||
|
- validité syntaxique Ansible.
|
||||||
|
|
||||||
|
## Ce qui doit rester validé par un humain
|
||||||
|
|
||||||
|
- lisibilité des rapports PDF;
|
||||||
|
- confort de navigation sur petit téléphone;
|
||||||
|
- ton des textes destinés aux membres;
|
||||||
|
- acceptabilité sociale des processus;
|
||||||
|
- clarté des formulaires papier;
|
||||||
|
- compréhension par des membres non techniques.
|
||||||
|
|
||||||
|
## Ronde de validation recommandée
|
||||||
|
|
||||||
|
1. Exécuter `scripts/validate.sh`.
|
||||||
|
2. Déployer sur une VM vanille.
|
||||||
|
3. Exécuter `ansible-playbook ansible/verify.yml`.
|
||||||
|
4. Réinitialiser `Groupe Démonstration`.
|
||||||
|
5. Parcourir les scénarios du [plan de validation fonctionnelle](96_PLAN_VALIDATION.md).
|
||||||
|
6. Noter les écarts avec la page, l'action, le résultat obtenu et le résultat attendu.
|
||||||
|
|
||||||
|
## Tests Playwright
|
||||||
|
|
||||||
|
Les tests Playwright sont dans `frontend/e2e`.
|
||||||
|
|
||||||
|
Ils démarrent automatiquement Vite, simulent les réponses API nécessaires et exécutent les scénarios dans un navigateur mobile et desktop.
|
||||||
|
|
||||||
|
Commande directe:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd frontend
|
||||||
|
npm run e2e
|
||||||
|
```
|
||||||
|
|
||||||
|
Pour installer le navigateur Chromium si nécessaire:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd frontend
|
||||||
|
npx playwright install chromium
|
||||||
|
```
|
||||||
|
|
||||||
|
## Évolution prévue
|
||||||
|
|
||||||
|
La prochaine étape pertinente est d'élargir Playwright vers les parcours authentifiés complets:
|
||||||
|
|
||||||
|
- sélection de groupe;
|
||||||
|
- connexion membre et sysadmin;
|
||||||
|
- navigation mobile;
|
||||||
|
- rencontre complète;
|
||||||
|
- assemblée complète;
|
||||||
|
- trésorerie;
|
||||||
|
- journal;
|
||||||
|
- captures de rapports PDF.
|
||||||
|
|
||||||
|
Ces tests devront être ajoutés progressivement pour éviter une suite fragile.
|
||||||
155
frontend/e2e/public.spec.js
Normal file
155
frontend/e2e/public.spec.js
Normal file
|
|
@ -0,0 +1,155 @@
|
||||||
|
import { expect, test } from '@playwright/test';
|
||||||
|
|
||||||
|
const groupes = [
|
||||||
|
{
|
||||||
|
id: 'groupe-alpha',
|
||||||
|
nom: 'Groupe Alpha',
|
||||||
|
district: '87-16',
|
||||||
|
adresse_local: '123 rue Principale, Saint-Bruno',
|
||||||
|
jour_reunion: 'mardi',
|
||||||
|
heure_reunion: '19:30',
|
||||||
|
assemblee_affaires_ordre: 'premiere',
|
||||||
|
format_reunion: 'Discussion ouverte',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'groupe-demo',
|
||||||
|
nom: 'Groupe Démonstration',
|
||||||
|
district: '87-16',
|
||||||
|
adresse_local: 'Salle de démonstration',
|
||||||
|
jour_reunion: 'samedi',
|
||||||
|
heure_reunion: '20:00',
|
||||||
|
assemblee_affaires_ordre: 'derniere',
|
||||||
|
format_reunion: 'Formation',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
async function mockPublicApi(page) {
|
||||||
|
await page.route('**/api/auth/groupes', async (route) => {
|
||||||
|
await route.fulfill({ json: groupes });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function mockSysadminApi(page) {
|
||||||
|
await page.route('**/api/admin/sysadmin-login', async (route) => {
|
||||||
|
const body = route.request().postDataJSON();
|
||||||
|
if (body.pin !== '0000') {
|
||||||
|
await route.fulfill({ status: 401, json: { detail: 'PIN Sysadmin invalide' } });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await route.fulfill({
|
||||||
|
json: {
|
||||||
|
access_token: 'instance-admin-token',
|
||||||
|
token_type: 'bearer',
|
||||||
|
sysadmin_instance: true,
|
||||||
|
membre: {
|
||||||
|
id: 'admin-id',
|
||||||
|
prenom: 'Sysadmin',
|
||||||
|
courriel: 'sysadmin',
|
||||||
|
groupe_id: null,
|
||||||
|
date_abstinence: null,
|
||||||
|
is_sysadmin: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
await page.route('**/api/admin/groupes', async (route) => {
|
||||||
|
await route.fulfill({
|
||||||
|
json: groupes.map((groupe, index) => ({
|
||||||
|
...groupe,
|
||||||
|
lien_aa87: 'https://aa87.org',
|
||||||
|
tenant_schema: `grp_${index}`,
|
||||||
|
cree_le: '2026-01-01T00:00:00Z',
|
||||||
|
membres: index === 0 ? 12 : 14,
|
||||||
|
reunions: index === 0 ? 52 : 52,
|
||||||
|
traces: index === 0 ? 120 : 180,
|
||||||
|
demo: groupe.nom === 'Groupe Démonstration',
|
||||||
|
protege: groupe.nom === 'Groupe Démonstration',
|
||||||
|
})),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
await page.route('**/api/admin/sysadmin-pin', async (route) => {
|
||||||
|
const body = route.request().postDataJSON();
|
||||||
|
if (body.pin_actuel !== '0000') {
|
||||||
|
await route.fulfill({ status: 401, json: { detail: 'PIN actuel invalide' } });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await route.fulfill({ json: { message: 'PIN sysadmin mis à jour' } });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
test.beforeEach(async ({ page }) => {
|
||||||
|
await page.addInitScript(() => {
|
||||||
|
localStorage.clear();
|
||||||
|
sessionStorage.clear();
|
||||||
|
});
|
||||||
|
await mockPublicApi(page);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('la page publique liste les groupes en tuiles fermées et expose les liens publics', async ({ page }) => {
|
||||||
|
await page.goto('/');
|
||||||
|
|
||||||
|
await expect(page.getByRole('heading', { name: 'Choisir un groupe' })).toBeVisible();
|
||||||
|
await expect(page.getByRole('button', { name: /Groupe Alpha/ })).toBeVisible();
|
||||||
|
await expect(page.getByText('123 rue Principale')).toBeHidden();
|
||||||
|
await expect(page.getByRole('button', { name: 'Sysadmin' })).toBeVisible();
|
||||||
|
await expect(page.getByRole('button', { name: 'Charte' })).toBeVisible();
|
||||||
|
await expect(page.getByRole('button', { name: 'Confidentialité' })).toBeVisible();
|
||||||
|
await expect(page.getByRole('link', { name: 'Découvrir' })).toBeVisible();
|
||||||
|
|
||||||
|
await page.getByRole('button', { name: /Groupe Alpha/ }).click();
|
||||||
|
|
||||||
|
await expect(page.getByText('123 rue Principale')).toBeVisible();
|
||||||
|
await expect(page.getByPlaceholder('Adresse courriel ou identifiant')).toBeVisible();
|
||||||
|
await expect(page.getByPlaceholder('PIN')).toHaveAttribute('type', 'password');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('les pages publiques Charte et Confidentialité restent accessibles sans connexion', async ({ page }) => {
|
||||||
|
await page.goto('/');
|
||||||
|
|
||||||
|
await page.getByRole('button', { name: 'Charte' }).click();
|
||||||
|
await expect(page.getByRole('heading', { name: /Charte|Principes/ })).toBeVisible();
|
||||||
|
|
||||||
|
await page.goto('/');
|
||||||
|
await page.getByRole('button', { name: 'Confidentialité' }).click();
|
||||||
|
await expect(page.getByRole('heading', { name: /Confidentialité/ })).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('le sysadmin d’instance ouvre la gestion des groupes sans contexte de groupe', async ({ page }) => {
|
||||||
|
await mockSysadminApi(page);
|
||||||
|
await page.goto('/');
|
||||||
|
|
||||||
|
await page.getByRole('button', { name: 'Sysadmin' }).click();
|
||||||
|
await page.getByPlaceholder('PIN sysadmin').fill('0000');
|
||||||
|
await page.getByRole('button', { name: 'Ouvrir' }).click();
|
||||||
|
|
||||||
|
await expect(page.getByRole('heading', { name: 'Gestion des groupes' })).toBeVisible();
|
||||||
|
await expect(page.getByRole('button', { name: 'Créer un groupe' })).toBeVisible();
|
||||||
|
await expect(page.getByRole('button', { name: /Groupe Alpha/ })).toBeVisible();
|
||||||
|
await expect(page.getByRole('button', { name: /Groupe Démonstration/ })).toBeVisible();
|
||||||
|
await expect(page.getByText('Instance · Bonjour Sysadmin')).toBeVisible();
|
||||||
|
await expect(page.getByText(/Groupe Alpha · Bonjour Sysadmin/)).not.toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('le changement de PIN sysadmin masque les saisies et exige une confirmation', async ({ page }) => {
|
||||||
|
await mockSysadminApi(page);
|
||||||
|
await page.goto('/');
|
||||||
|
|
||||||
|
await page.getByRole('button', { name: 'Sysadmin' }).click();
|
||||||
|
await page.getByPlaceholder('PIN sysadmin').fill('0000');
|
||||||
|
await page.getByRole('button', { name: 'Ouvrir' }).click();
|
||||||
|
await page.getByRole('button', { name: 'Changer le PIN sysadmin' }).click();
|
||||||
|
|
||||||
|
await expect(page.getByPlaceholder('PIN actuel')).toHaveAttribute('type', 'password');
|
||||||
|
await expect(page.getByPlaceholder('Nouveau PIN', { exact: true })).toHaveAttribute('type', 'password');
|
||||||
|
await expect(page.getByPlaceholder('Confirmer le nouveau PIN')).toHaveAttribute('type', 'password');
|
||||||
|
|
||||||
|
await page.getByPlaceholder('PIN actuel').fill('0000');
|
||||||
|
await page.getByPlaceholder('Nouveau PIN', { exact: true }).fill('1234');
|
||||||
|
await page.getByPlaceholder('Confirmer le nouveau PIN').fill('4321');
|
||||||
|
await page.getByRole('button', { name: 'Enregistrer' }).click();
|
||||||
|
await expect(page.getByText('La confirmation ne correspond pas')).toBeVisible();
|
||||||
|
|
||||||
|
await page.getByPlaceholder('Confirmer le nouveau PIN').fill('1234');
|
||||||
|
await page.getByRole('button', { name: 'Enregistrer' }).click();
|
||||||
|
await expect(page.getByText('PIN sysadmin mis à jour')).toBeVisible();
|
||||||
|
});
|
||||||
60
frontend/package-lock.json
generated
60
frontend/package-lock.json
generated
|
|
@ -13,6 +13,7 @@
|
||||||
"zustand": "^5.0.0"
|
"zustand": "^5.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@playwright/test": "^1.60.0",
|
||||||
"@types/react": "^18.3.0",
|
"@types/react": "^18.3.0",
|
||||||
"@types/react-dom": "^18.3.0",
|
"@types/react-dom": "^18.3.0",
|
||||||
"@vitejs/plugin-react": "^4.3.0",
|
"@vitejs/plugin-react": "^4.3.0",
|
||||||
|
|
@ -1999,6 +2000,21 @@
|
||||||
"node": ">= 8"
|
"node": ">= 8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@playwright/test": {
|
||||||
|
"version": "1.60.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.60.0.tgz",
|
||||||
|
"integrity": "sha512-O71yZIbAh/PxDMNGns37GHBIfrVkEVyn+AXyIa5dOTfb4/xNvRWV+Vv/NMbNCtODB/pO7vLlF2OTmMVLhmr7Ag==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"playwright": "1.60.0"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"playwright": "cli.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@rolldown/pluginutils": {
|
"node_modules/@rolldown/pluginutils": {
|
||||||
"version": "1.0.0-beta.27",
|
"version": "1.0.0-beta.27",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz",
|
||||||
|
|
@ -4753,6 +4769,50 @@
|
||||||
"node": ">= 6"
|
"node": ">= 6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/playwright": {
|
||||||
|
"version": "1.60.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.60.0.tgz",
|
||||||
|
"integrity": "sha512-hheHdokM8cdqCb0lcE3s+zT4t4W+vvjpGxsZlDnikarzx8tSzMebh3UiFtgqwFwnTnjYQcsyMF8ei2mCO/tpeA==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"playwright-core": "1.60.0"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"playwright": "cli.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"fsevents": "2.3.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/playwright-core": {
|
||||||
|
"version": "1.60.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.60.0.tgz",
|
||||||
|
"integrity": "sha512-9bW6zvX/m0lEbgTKJ6YppOKx8H3VOPBMOCFh2irXFOT4BbHgrx5hPjwJYLT40Lu+4qtD36qKc/Hn56StUW57IA==",
|
||||||
|
"dev": true,
|
||||||
|
"bin": {
|
||||||
|
"playwright-core": "cli.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/playwright/node_modules/fsevents": {
|
||||||
|
"version": "2.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
|
||||||
|
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
|
||||||
|
"dev": true,
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/possible-typed-array-names": {
|
"node_modules/possible-typed-array-names": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz",
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,8 @@
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
"preview": "vite preview"
|
"preview": "vite preview",
|
||||||
|
"e2e": "playwright test"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
|
|
@ -15,12 +16,13 @@
|
||||||
"zustand": "^5.0.0"
|
"zustand": "^5.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@playwright/test": "^1.60.0",
|
||||||
"@types/react": "^18.3.0",
|
"@types/react": "^18.3.0",
|
||||||
"@types/react-dom": "^18.3.0",
|
"@types/react-dom": "^18.3.0",
|
||||||
"@vitejs/plugin-react": "^4.3.0",
|
"@vitejs/plugin-react": "^4.3.0",
|
||||||
"tailwindcss": "^3.4.0",
|
|
||||||
"autoprefixer": "^10.4.0",
|
"autoprefixer": "^10.4.0",
|
||||||
"postcss": "^8.4.0",
|
"postcss": "^8.4.0",
|
||||||
|
"tailwindcss": "^3.4.0",
|
||||||
"vite": "^5.4.0",
|
"vite": "^5.4.0",
|
||||||
"vite-plugin-pwa": "^0.20.0"
|
"vite-plugin-pwa": "^0.20.0"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
37
frontend/playwright.config.js
Normal file
37
frontend/playwright.config.js
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
import { defineConfig, devices } from '@playwright/test';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
testDir: './e2e',
|
||||||
|
timeout: 30_000,
|
||||||
|
expect: { timeout: 5_000 },
|
||||||
|
fullyParallel: true,
|
||||||
|
reporter: [['list']],
|
||||||
|
use: {
|
||||||
|
baseURL: 'http://127.0.0.1:5173',
|
||||||
|
trace: 'on-first-retry',
|
||||||
|
screenshot: 'only-on-failure',
|
||||||
|
},
|
||||||
|
webServer: {
|
||||||
|
command: 'npm run dev -- --host 127.0.0.1',
|
||||||
|
url: 'http://127.0.0.1:5173',
|
||||||
|
reuseExistingServer: !process.env.CI,
|
||||||
|
timeout: 120_000,
|
||||||
|
},
|
||||||
|
projects: [
|
||||||
|
{
|
||||||
|
name: 'mobile-chromium',
|
||||||
|
use: {
|
||||||
|
browserName: 'chromium',
|
||||||
|
viewport: { width: 390, height: 844 },
|
||||||
|
isMobile: true,
|
||||||
|
hasTouch: true,
|
||||||
|
deviceScaleFactor: 3,
|
||||||
|
userAgent: devices['iPhone 13'].userAgent,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'desktop-chromium',
|
||||||
|
use: { ...devices['Desktop Chrome'] },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
41
scripts/validate.sh
Executable file
41
scripts/validate.sh
Executable file
|
|
@ -0,0 +1,41 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
cd "$ROOT"
|
||||||
|
mkdir -p .tmp/ansible-local .tmp/ansible-remote
|
||||||
|
export ANSIBLE_LOCAL_TEMP="$ROOT/.tmp/ansible-local"
|
||||||
|
export ANSIBLE_REMOTE_TEMP="$ROOT/.tmp/ansible-remote"
|
||||||
|
|
||||||
|
echo "== Python syntax =="
|
||||||
|
python3 -m py_compile $(find backend/app deploy -name '*.py' -print)
|
||||||
|
|
||||||
|
echo "== Backend tests =="
|
||||||
|
if [ -x ".venv/bin/python" ]; then
|
||||||
|
.venv/bin/python -m pytest backend/tests
|
||||||
|
else
|
||||||
|
python3 -m pytest backend/tests
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "== Frontend build =="
|
||||||
|
(cd frontend && npm run build)
|
||||||
|
|
||||||
|
if [ "${SKIP_E2E:-0}" = "1" ]; then
|
||||||
|
echo "== Playwright E2E =="
|
||||||
|
echo "SKIP_E2E=1; tests navigateur ignorés."
|
||||||
|
else
|
||||||
|
echo "== Playwright E2E =="
|
||||||
|
(cd frontend && npm run e2e)
|
||||||
|
fi
|
||||||
|
|
||||||
|
if command -v ansible-playbook >/dev/null 2>&1; then
|
||||||
|
echo "== Ansible syntax =="
|
||||||
|
ansible-playbook ansible/site.yml --syntax-check
|
||||||
|
ansible-playbook ansible/verify.yml --syntax-check
|
||||||
|
ansible-playbook ansible/backup-now.yml --syntax-check
|
||||||
|
else
|
||||||
|
echo "== Ansible syntax =="
|
||||||
|
echo "ansible-playbook introuvable; syntax-check ignoré."
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Validation locale terminée."
|
||||||
Loading…
Reference in a new issue