Paillasse/portail/tests/Reels/visio-e2e.mjs
Adam Belkacemi ec4eae3744 ajoute la visio de classe : Jitsi auto-hébergé, jetons du portail, prof seul modérateur
Jitsi (docker-jitsi-meet, version figée) derrière Nginx sur meet.syllabuslab.fr,
accès par jeton JWT signé par le portail, modération du micro, mains levées
synchronisées avec l'application. Pare-feu : UDP 10000 et pont interne
br-visio (accords d'Adam). Jicofo n'authentifie plus lui-même : il donnait
le rôle de propriétaire aux élèves. Capacité ramenée à 20 VM.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 12:35:35 +02:00

91 lines
4.8 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Critère de l'étape 15, dans de vrais navigateurs (Chromium, caméra et micro
// simulés) : un prof et deux élèves rejoignent la visio ; un élève lève la
// main ; le prof lui donne la parole ; un intrus sans jeton ne peut pas entrer.
// node visio-e2e.mjs <fichier d'identifiants> [https://syllabuslab.fr]
import { chromium } from 'playwright';
import { readFileSync } from 'node:fs';
const ids = JSON.parse(readFileSync(process.argv[2], 'utf8'));
const BASE = process.argv[3] ?? 'https://syllabuslab.fr';
const S = ids.seance_id;
const navigateur = await chromium.launch({ args: ['--use-fake-ui-for-media-stream', '--use-fake-device-for-media-stream'] });
const contexte = () => navigateur.newContext({ locale: 'fr-FR', permissions: ['camera', 'microphone'] });
const etape = (texte) => console.log(`${new Date().toISOString().slice(11, 19)} ${texte}`);
const attendre = async (fn, delai = 30000, pas = 500) => {
const fin = Date.now() + delai;
while (Date.now() < fin) { const r = await fn(); if (r) return r; await new Promise((ok) => setTimeout(ok, pas)); }
throw new Error('délai dépassé');
};
async function connexion(page, champs, bouton) {
await page.goto(`${BASE}/connexion`);
for (const [nom, valeur] of Object.entries(champs)) await page.fill(`input[name="${nom}"]`, valeur);
await page.click(bouton);
await page.waitForLoadState('networkidle');
}
try {
// 1. Le prof ouvre la visio.
const prof = await (await contexte()).newPage();
await connexion(prof, { email: ids.prof.email, password: ids.prof.mot_de_passe }, 'form[action$="/connexion/prof"] button[type=submit]');
await prof.goto(`${BASE}/seances/${S}/visio`);
if (await prof.locator('text=Ouvrir la visio').count()) await prof.click('text=Ouvrir la visio');
await prof.waitForSelector('#v-scene iframe', { timeout: 30000 });
etape('prof : visio ouverte, Jitsi chargé');
// 2. Deux élèves la rejoignent.
const eleves = [];
for (const e of ids.eleves) {
const page = await (await contexte()).newPage();
await connexion(page, { identifiant: e.identifiant, code: e.code }, 'form[action$="/connexion/eleve"] button[type=submit]');
await page.goto(`${BASE}/seances/${S}/visio/eleve`);
await page.waitForSelector('#v-scene iframe', { timeout: 30000 });
eleves.push(page);
}
const enLigne = await attendre(async () => {
const t = await prof.textContent('#v-nb');
return /3 en ligne/.test(t ?? '') && t;
}, 60000);
etape(`prof : ${enLigne.trim()} (le prof et les deux élèves)`);
// 3. Léa lève la main ; elle apparaît chez le prof.
const [lea, yanis] = eleves;
await lea.click('[data-main-e]');
await attendre(async () => (await prof.locator('#v-mains [data-parole]').count()) === 1, 30000);
etape(`prof : main levée reçue de « ${(await prof.locator('#v-mains strong').first().textContent())} »`);
// 4. Yanis, sans la parole, ne peut pas réactiver son micro.
await yanis.click('[data-micro-e]');
await new Promise((ok) => setTimeout(ok, 4000));
const statutYanis = (await yanis.textContent('[data-statut-e]')).trim();
etape(`Yanis tente d'ouvrir son micro sans la parole → « ${statutYanis} »`);
// 5. Le prof donne la parole à Léa : elle peut ouvrir son micro.
await prof.click('#v-mains [data-parole]');
await attendre(async () => (await lea.getAttribute('[data-main-e]', 'aria-pressed')) === 'false', 30000);
etape('Léa : sa main est baissée (parole donnée)');
await lea.click('[data-micro-e]');
const statutLea = await attendre(async () => {
const t = (await lea.textContent('[data-statut-e]')).trim();
return t === 'Tu as la parole' && t;
}, 30000);
etape(`Léa ouvre son micro → « ${statutLea} »`);
// 6. Un intrus, sans jeton, tente d'entrer directement dans la salle.
const salle = await prof.evaluate(() => document.querySelector('#v-scene iframe').src.split('?')[0].split('#')[0].split('/').pop());
const intrus = await (await contexte()).newPage();
await intrus.goto(`${BASE.replace('://', '://meet.')}/${salle}`);
await new Promise((ok) => setTimeout(ok, 12000));
const toujours = (await prof.textContent('#v-nb')).trim();
const ecranIntrus = (await intrus.locator('body').innerText()).replace(/\s+/g, ' ').slice(0, 160);
etape(`intrus sans jeton : le prof voit toujours « ${toujours} » ; écran de l'intrus : « ${ecranIntrus} »`);
const ok = /3 en ligne/.test(toujours) && statutYanis === 'En sourdine' && statutLea === 'Tu as la parole';
console.log(ok ? 'CRITÈRE DE L’ÉTAPE 15 : RÉUSSI' : 'CRITÈRE DE L’ÉTAPE 15 : ÉCHOUÉ');
process.exitCode = ok ? 0 : 1;
} catch (e) {
console.error('ÉCHEC :', e.message);
process.exitCode = 1;
} finally {
await navigateur.close();
}