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/
|
||||
dist/
|
||||
__pycache__/
|
||||
.tmp/
|
||||
frontend/test-results/
|
||||
frontend/playwright-report/
|
||||
*.py[cod]
|
||||
*.retry
|
||||
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`
|
||||
- Déploiement Ansible: `docs/91_DEPLOIEMENT_ANSIBLE.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`
|
||||
|
||||
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:
|
||||
|
||||
```bash
|
||||
scripts/validate.sh
|
||||
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
|
||||
(cd frontend && npm run e2e)
|
||||
ansible-playbook ansible/site.yml --syntax-check
|
||||
ansible-playbook ansible/verify.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)
|
||||
- [Sécurité et confidentialité](80_SECURITE.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:
|
||||
|
||||
|
|
@ -31,6 +33,8 @@ Pour maintenir ou faire évoluer le code:
|
|||
- [API applicative](70_API.md)
|
||||
- [Règles métier](30_REGLES_METIER.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
|
||||
|
||||
|
|
|
|||
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"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.60.0",
|
||||
"@types/react": "^18.3.0",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"@vitejs/plugin-react": "^4.3.0",
|
||||
|
|
@ -1999,6 +2000,21 @@
|
|||
"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": {
|
||||
"version": "1.0.0-beta.27",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz",
|
||||
|
|
@ -4753,6 +4769,50 @@
|
|||
"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": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz",
|
||||
|
|
|
|||
|
|
@ -7,7 +7,8 @@
|
|||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
"preview": "vite preview",
|
||||
"e2e": "playwright test"
|
||||
},
|
||||
"dependencies": {
|
||||
"react": "^18.3.1",
|
||||
|
|
@ -15,12 +16,13 @@
|
|||
"zustand": "^5.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.60.0",
|
||||
"@types/react": "^18.3.0",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"@vitejs/plugin-react": "^4.3.0",
|
||||
"tailwindcss": "^3.4.0",
|
||||
"autoprefixer": "^10.4.0",
|
||||
"postcss": "^8.4.0",
|
||||
"tailwindcss": "^3.4.0",
|
||||
"vite": "^5.4.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