141 lines
5.9 KiB
JavaScript
141 lines
5.9 KiB
JavaScript
|
|
// © 2026 TechnoLibre (http://www.technolibre.ca)
|
|||
|
|
// License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
|
|||
|
|
|
|||
|
|
// La coquille Electron (§ 13.1) : la fenêtre qu'ouvre le processus principal,
|
|||
|
|
// la page qu'elle charge, le pont étroit que le préchargement expose, et le
|
|||
|
|
// trajet du dossier publié par le lanceur portable jusqu'à la page (§ 8.6,
|
|||
|
|
// point 1).
|
|||
|
|
//
|
|||
|
|
// electron/main.js s'exécute sous Node, dans un processus à part, contre
|
|||
|
|
// test/electron_factice.js, qui consigne ce que la coquille demande à
|
|||
|
|
// Electron puis rend son journal en JSON. Ce qui s'éprouve ici est ce que la
|
|||
|
|
// coquille demande ; qu'Electron l'applique relève d'une épreuve dans un vrai
|
|||
|
|
// Electron.
|
|||
|
|
import assert from 'node:assert/strict';
|
|||
|
|
import { spawnSync } from 'node:child_process';
|
|||
|
|
import { existsSync, mkdtempSync, rmSync } from 'node:fs';
|
|||
|
|
import { tmpdir } from 'node:os';
|
|||
|
|
import { join } from 'node:path';
|
|||
|
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|||
|
|
import { describe, test } from './lanceur.js';
|
|||
|
|
|
|||
|
|
const RACINE = fileURLToPath(new URL('..', import.meta.url));
|
|||
|
|
const FACTICE = pathToFileURL(join(RACINE, 'test', 'electron_factice.js')).href;
|
|||
|
|
const PRINCIPAL = join(RACINE, 'electron', 'main.js');
|
|||
|
|
const PRECHARGEMENT = join(RACINE, 'electron', 'preload.cjs');
|
|||
|
|
const CANAL = 'gtt:dossier-executable';
|
|||
|
|
|
|||
|
|
// Délai de l'exécution de la coquille. Le factice ne joue la page qu'une
|
|||
|
|
// fois le processus sans rien à faire ; un minuteur ou un écouteur laissé
|
|||
|
|
// actif l'en empêche, et l'exécution ne finit jamais. Le délai reste sous
|
|||
|
|
// celui d'une épreuve de Vitest, 5 s, qui n'interrompt pas un appel
|
|||
|
|
// synchrone : l'épreuve échoue en le disant, au lieu de figer la série. Le
|
|||
|
|
// délai écoulé, le processus reçoit KILL, qu'aucun gestionnaire de signal
|
|||
|
|
// ne retient.
|
|||
|
|
const DELAI_MS = 4_000;
|
|||
|
|
|
|||
|
|
// Exécute la coquille contre le factice et rend son journal. Le répertoire
|
|||
|
|
// courant est un dossier vide hors du projet : dans l'exécutable, il n'est
|
|||
|
|
// jamais l'intérieur d'app.asar, si bien qu'un chemin résolu contre lui ne
|
|||
|
|
// désigne ni la page ni le préchargement.
|
|||
|
|
function executerLaCoquille() {
|
|||
|
|
const environnement = { ...process.env };
|
|||
|
|
delete environnement.PORTABLE_EXECUTABLE_DIR;
|
|||
|
|
const ailleurs = mkdtempSync(join(tmpdir(), 'coquille-'));
|
|||
|
|
try {
|
|||
|
|
const resultat = spawnSync(process.execPath, ['--import', FACTICE, PRINCIPAL], {
|
|||
|
|
cwd: ailleurs,
|
|||
|
|
encoding: 'utf8',
|
|||
|
|
env: environnement,
|
|||
|
|
timeout: DELAI_MS,
|
|||
|
|
killSignal: 'SIGKILL',
|
|||
|
|
});
|
|||
|
|
assert.notEqual(
|
|||
|
|
resultat.error?.code,
|
|||
|
|
'ETIMEDOUT',
|
|||
|
|
`la coquille ne s'arrête pas en ${DELAI_MS} ms : un minuteur ou un écouteur la tient en vie`,
|
|||
|
|
);
|
|||
|
|
assert.equal(resultat.status, 0, `la coquille ne s'exécute pas :\n${resultat.stderr}`);
|
|||
|
|
return JSON.parse(resultat.stdout);
|
|||
|
|
} finally {
|
|||
|
|
rmSync(ailleurs, { recursive: true, force: true });
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// L'exécution est partagée par les épreuves : le processus ne se lance
|
|||
|
|
// qu'une fois, et son échec se rend à chacune sans relancer.
|
|||
|
|
let execution;
|
|||
|
|
function journalDeLaCoquille() {
|
|||
|
|
if (execution === undefined) {
|
|||
|
|
try {
|
|||
|
|
execution = { journal: executerLaCoquille() };
|
|||
|
|
} catch (erreur) {
|
|||
|
|
execution = { erreur };
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
if (execution.erreur !== undefined) throw execution.erreur;
|
|||
|
|
return execution.journal;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
describe('coquille : processus principal', () => {
|
|||
|
|
test("une seule fenêtre, 1366 × 768 : page isolée, sans Node, en bac à sable, préchargée par le pont", () => {
|
|||
|
|
const { fenetres } = journalDeLaCoquille();
|
|||
|
|
assert.equal(fenetres.length, 1);
|
|||
|
|
const [{ width, height, webPreferences }] = fenetres;
|
|||
|
|
assert.deepEqual({ width, height }, { width: 1366, height: 768 });
|
|||
|
|
assert.deepEqual(webPreferences, {
|
|||
|
|
preload: PRECHARGEMENT,
|
|||
|
|
contextIsolation: true,
|
|||
|
|
nodeIntegration: false,
|
|||
|
|
sandbox: true,
|
|||
|
|
});
|
|||
|
|
assert.ok(existsSync(webPreferences.preload), `${webPreferences.preload} absent`);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test('la fenêtre charge la page que Vite construit dans www/', () => {
|
|||
|
|
assert.deepEqual(journalDeLaCoquille().chargements, [
|
|||
|
|
{ loadFile: join(RACINE, 'www', 'index.html') },
|
|||
|
|
]);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test(`un seul canal IPC, ${CANAL}, posé avant le chargement de la page`, () => {
|
|||
|
|
const { canaux, sequence } = journalDeLaCoquille();
|
|||
|
|
assert.deepEqual(canaux, [CANAL]);
|
|||
|
|
const pose = sequence.indexOf(`ipcMain.handle:${CANAL}`);
|
|||
|
|
const charge = sequence.indexOf('loadFile');
|
|||
|
|
assert.ok(pose !== -1 && charge !== -1 && pose < charge, `ordre des appels : ${sequence.join(', ')}`);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test("la page n'ouvre aucune fenêtre et ne navigue nulle part", () => {
|
|||
|
|
const { ouvertures, navigations } = journalDeLaCoquille();
|
|||
|
|
assert.deepEqual(ouvertures, [{ action: 'deny' }]);
|
|||
|
|
assert.ok(navigations.length > 0, 'aucun écouteur de will-navigate');
|
|||
|
|
assert.deepEqual(navigations, navigations.map(() => true));
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
describe('coquille : préchargement', () => {
|
|||
|
|
test("n'expose que window.gtt, et ne requiert qu'electron", () => {
|
|||
|
|
const { prechargement } = journalDeLaCoquille();
|
|||
|
|
assert.equal(prechargement.erreur, undefined, prechargement.erreur);
|
|||
|
|
assert.deepEqual(prechargement.requis, ['electron']);
|
|||
|
|
assert.deepEqual(prechargement.exposes, [
|
|||
|
|
{ cle: 'gtt', membres: ['dossierExecutable', 'plateforme'] },
|
|||
|
|
]);
|
|||
|
|
assert.equal(prechargement.plateforme, 'electron');
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test(`gtt.dossierExecutable() demande à chaque appel, par ${CANAL}, le dossier publié par le lanceur portable, null sans lui`, () => {
|
|||
|
|
const { prechargement, invocations } = journalDeLaCoquille();
|
|||
|
|
assert.equal(prechargement.erreur, undefined, prechargement.erreur);
|
|||
|
|
assert.deepEqual(prechargement.reponses, {
|
|||
|
|
publie: { valeur: 'E:\\soirees' },
|
|||
|
|
absent: { valeur: null },
|
|||
|
|
vide: { valeur: null },
|
|||
|
|
});
|
|||
|
|
// Le préchargement voit lui aussi process.env : seules les invocations
|
|||
|
|
// montrent que la réponse vient du processus principal, une par appel.
|
|||
|
|
assert.deepEqual(invocations, [CANAL, CANAL, CANAL]);
|
|||
|
|
});
|
|||
|
|
});
|