Test dans les navigateurs : CSP vérifiée, diagnostic des blocages
make test-browser vérifie maintenant la politique de sécurité dans Chromium et Firefox : aucune violation pendant toute la partie, et un script en ligne ajouté exprès est bloqué. Un blocage est diagnostiqué (étape, capture, dernière commande du moteur, journal de la partie) au lieu d'attendre sans fin. Firefox sous Playwright refuse toute navigation hors ligne : on y vérifie le cache et le contrôle par le service worker. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
9cc0e0ccd5
commit
2c43a0a69f
+119
-6
@@ -26,17 +26,51 @@ const context = await browser.newContext({
|
||||
deviceScaleFactor: 2,
|
||||
locale: "fr-FR",
|
||||
});
|
||||
// Pour le diagnostic d'un blocage : la dernière commande envoyée au moteur
|
||||
// (Web Worker), et si elle a reçu sa réponse ; et, pour vérifier la
|
||||
// politique de sécurité du contenu, chaque violation signalée par la page.
|
||||
await context.addInitScript(() => {
|
||||
window.__engine = { sent: 0, received: 0, last: null };
|
||||
const post = Worker.prototype.postMessage;
|
||||
Worker.prototype.postMessage = function (message, ...rest) {
|
||||
window.__engine.sent++;
|
||||
window.__engine.last = message?.args ?? null;
|
||||
if (!this.__counted) {
|
||||
this.__counted = true;
|
||||
this.addEventListener("message", () => window.__engine.received++);
|
||||
}
|
||||
return post.call(this, message, ...rest);
|
||||
};
|
||||
window.__csp = [];
|
||||
document.addEventListener("securitypolicyviolation", (e) =>
|
||||
window.__csp.push(`${e.effectiveDirective} ${e.blockedURI}`),
|
||||
);
|
||||
});
|
||||
const page = await context.newPage();
|
||||
const problems = [];
|
||||
page.on("pageerror", (error) => problems.push(`erreur de la page : ${error.message}`));
|
||||
page.on("crash", () => {
|
||||
console.error(`[${browserName}] la page a planté`);
|
||||
process.exit(1);
|
||||
});
|
||||
// Vrai pendant le contrôle final de la politique de sécurité, qui provoque
|
||||
// exprès une violation.
|
||||
let cspProbe = false;
|
||||
page.on("console", (message) => {
|
||||
if (message.type() === "error") {
|
||||
if (message.type() === "error" && !(cspProbe && /Content.Security.Policy/i.test(message.text()))) {
|
||||
problems.push(`console : ${message.text()}`);
|
||||
}
|
||||
});
|
||||
const log = (text) => console.log(`[${browserName}] ${text}`);
|
||||
const shot = (name) => page.screenshot({ path: `${shots}/${browserName}-${name}.png` });
|
||||
|
||||
/** Relève les violations de la CSP signalées par la page (avant chaque
|
||||
* rechargement, qui les efface). */
|
||||
const violations = [];
|
||||
async function collectViolations() {
|
||||
violations.push(...(await page.evaluate(() => window.__csp)));
|
||||
}
|
||||
|
||||
function check(condition, message) {
|
||||
if (!condition) {
|
||||
throw new Error(message);
|
||||
@@ -79,9 +113,40 @@ async function waitForPlayer() {
|
||||
}
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
}
|
||||
throw new Error("rien à faire pour le joueur depuis 60 s");
|
||||
await shot("bloque").catch(() => {});
|
||||
const screen = await page
|
||||
.evaluate(() => {
|
||||
const text = (id) => document.getElementById(id)?.textContent;
|
||||
const busy = document.body.classList.contains("busy") ? "occupé" : "libre";
|
||||
const engine = window.__engine;
|
||||
return (
|
||||
`${busy} ; « ${text("info")} » ; « ${text("prompt")} » ; moteur : ` +
|
||||
`${engine.sent} commandes, ${engine.received} réponses, dernière ` +
|
||||
`${JSON.stringify(engine.last)} ; journal ` +
|
||||
localStorage.getItem("ustagammon.current-game")
|
||||
);
|
||||
})
|
||||
.catch((error) => `page illisible (${error.message})`);
|
||||
throw new Error(`rien à faire pour le joueur depuis 60 s : ${screen}`);
|
||||
}
|
||||
|
||||
// Garde-fou : une étape bloquée plus de deux minutes arrête tout, avec une
|
||||
// capture et l'état de l'écran.
|
||||
let lastStep = Date.now();
|
||||
let stepName = "chargement";
|
||||
function step(name) {
|
||||
lastStep = Date.now();
|
||||
stepName = name;
|
||||
}
|
||||
const watchdog = setInterval(async () => {
|
||||
if (Date.now() - lastStep > 120_000) {
|
||||
clearInterval(watchdog);
|
||||
console.error(`[${browserName}] bloqué depuis 2 minutes (étape : ${stepName})`);
|
||||
await shot("bloque").catch(() => {});
|
||||
process.exit(1);
|
||||
}
|
||||
}, 5_000);
|
||||
|
||||
const start = Date.now();
|
||||
await page.goto(base);
|
||||
const first = await waitForPlayer();
|
||||
@@ -95,6 +160,10 @@ let computerMs = [];
|
||||
for (let actions = 0; ; actions++) {
|
||||
check(actions < 800, "la partie ne finit pas");
|
||||
const todo = await waitForPlayer();
|
||||
step(`partie, action ${actions}`);
|
||||
if (process.env.TRACE) {
|
||||
log(`${todo} : ${await page.textContent("#info")} / ${await page.textContent("#prompt")}`);
|
||||
}
|
||||
const info = await page.textContent("#info");
|
||||
const timing = /\((\d+),(\d) s\)/.exec(info ?? "");
|
||||
if (timing) {
|
||||
@@ -136,6 +205,7 @@ for (let actions = 0; ; actions++) {
|
||||
await shot("3-milieu-de-partie");
|
||||
const before = await savedJournal();
|
||||
const pips = await page.textContent("#pips");
|
||||
await collectViolations();
|
||||
await page.reload();
|
||||
await waitForPlayer();
|
||||
const after = await savedJournal();
|
||||
@@ -159,6 +229,7 @@ if (computerMs.length > 0) {
|
||||
}
|
||||
await shot("4-fin-de-partie");
|
||||
|
||||
step("aide");
|
||||
// Aide : scénario et glossaire lus dans les copies de docs/.
|
||||
await page.click("#help-button");
|
||||
await page.waitForSelector(".glossary dt");
|
||||
@@ -169,6 +240,7 @@ log(`aide : ${scenarios} scénarios, ${terms} termes du glossaire`);
|
||||
await shot("5-aide");
|
||||
await page.click("#help-back");
|
||||
|
||||
step("thème sombre");
|
||||
// Thème sombre.
|
||||
await page.emulateMedia({ colorScheme: "dark" });
|
||||
await page.click("#again");
|
||||
@@ -176,16 +248,19 @@ await waitForPlayer();
|
||||
await shot("6-sombre");
|
||||
await page.emulateMedia({ colorScheme: "light" });
|
||||
|
||||
step("mise en cache");
|
||||
// Hors ligne : une fois le cache du service worker rempli, la page se
|
||||
// recharge sans réseau, et le moteur répond.
|
||||
const cached = await page.evaluate(async () => {
|
||||
await navigator.serviceWorker.ready;
|
||||
// Le service worker est enregistré une fois le moteur chargé ; s'il ne
|
||||
// s'installe pas, l'attente s'arrête au bout d'une minute.
|
||||
for (let i = 0; i < 600; i++) {
|
||||
const names = await caches.keys();
|
||||
if (names.length === 1) {
|
||||
const cache = await caches.open(names[0]);
|
||||
const keys = await cache.keys();
|
||||
if (keys.some((r) => r.url.endsWith("/wasm/engine.data"))) {
|
||||
const active = (await navigator.serviceWorker.getRegistration())?.active;
|
||||
if (active && keys.some((r) => r.url.endsWith("/wasm/engine.data"))) {
|
||||
return keys.length;
|
||||
}
|
||||
}
|
||||
@@ -194,8 +269,19 @@ const cached = await page.evaluate(async () => {
|
||||
return 0;
|
||||
});
|
||||
check(cached > 0, "le service worker n'a pas mis le site en cache");
|
||||
await context.setOffline(true);
|
||||
// Firefox sous Playwright refuse toute navigation une fois le contexte
|
||||
// « hors ligne », même servie par le service worker (NS_ERROR_OFFLINE) : on
|
||||
// y vérifie seulement que la page rechargée est contrôlée par le service
|
||||
// worker, dont le cache est complet.
|
||||
const offlineMode = browserName === "chromium";
|
||||
if (offlineMode) {
|
||||
await context.setOffline(true);
|
||||
}
|
||||
step("rechargement hors ligne");
|
||||
await collectViolations();
|
||||
await page.reload();
|
||||
const controlled = await page.evaluate(() => navigator.serviceWorker.controller !== null);
|
||||
check(controlled, "la page rechargée n'est pas contrôlée par le service worker");
|
||||
const offline = await waitForPlayer();
|
||||
if (offline === "roll") {
|
||||
await page.click("#roll");
|
||||
@@ -204,9 +290,36 @@ if (offline === "roll") {
|
||||
await page.click("#help-button");
|
||||
await page.waitForSelector(".glossary dt");
|
||||
await page.click("#help-back");
|
||||
log(`hors ligne : page rechargée depuis le cache (${cached} adresses), moteur et aide disponibles`);
|
||||
log(
|
||||
offlineMode
|
||||
? `hors ligne : page rechargée sans réseau depuis le cache (${cached} adresses), moteur et aide disponibles`
|
||||
: `cache du service worker rempli (${cached} adresses), page rechargée sous son contrôle ` +
|
||||
"(le hors ligne lui-même n'est pas simulable dans Firefox sous Playwright)",
|
||||
);
|
||||
await shot("7-hors-ligne");
|
||||
|
||||
step("CSP");
|
||||
// Politique de sécurité du contenu : aucune violation pendant toute la
|
||||
// partie (moteur WebAssembly, Web Worker, service worker, aide compris) ; et
|
||||
// elle est bien appliquée : un script en ligne ajouté à la page est bloqué.
|
||||
await collectViolations();
|
||||
check(violations.length === 0, `violations de la CSP : ${violations.join(", ")}`);
|
||||
cspProbe = true;
|
||||
const probe = await page.evaluate(async () => {
|
||||
const script = document.createElement("script");
|
||||
script.textContent = "window.__inline = true;";
|
||||
document.head.append(script);
|
||||
await new Promise((resolve) => setTimeout(resolve, 300));
|
||||
return { ran: window.__inline === true, violations: window.__csp };
|
||||
});
|
||||
check(!probe.ran, "CSP non appliquée : un script en ligne a été exécuté");
|
||||
check(
|
||||
probe.violations.some((v) => v.startsWith("script-src")),
|
||||
"CSP : le script en ligne bloqué n'a pas été signalé",
|
||||
);
|
||||
log("CSP : aucune violation pendant la partie, script en ligne bloqué");
|
||||
|
||||
clearInterval(watchdog);
|
||||
await browser.close();
|
||||
if (problems.length > 0) {
|
||||
console.error(problems.join("\n"));
|
||||
|
||||
Reference in New Issue
Block a user