Printing goes through Chromium's PDF with the CSS page size; a browser command reads the PDF under node and returns pages, MediaBox and text only. The tests prove pages = sheets for the cut plan in A4 and Letter, the paper size within 0.25 mm, BROUILLON and the placement timestamp on every page of a draft and none of a retained plan. The startup trial prints the preview through the real shell. A table sheet whose tours do not fit side by side splits them into groups of consecutive tours, so a long name wraps inside its page instead of being clipped. Checked: node 3776, browser 753, node-long 101; startup trial conforms. --- FR --- [ADD] impression : PDF réel prouvé, feuilles par table en groupes L'impression passe par le PDF de Chromium à la taille de page CSS ; une commande lit le PDF sous node et ne rend que pages, MediaBox et textes. Les épreuves prouvent pages = feuilles du plan découpé en A4 et Lettre, le papier à 0,25 mm, BROUILLON et l'horodatage du placement sur chaque page d'un brouillon et sur aucune d'un retenu. L'essai de démarrage imprime l'aperçu par la vraie coquille. Une feuille par table dont les tours ne tiennent pas côte à côte les répartit en groupes consécutifs : un long nom passe à la ligne dans sa page au lieu d'être coupé. Vérifié : node 3776, navigateur 753, longues 101 ; essai conforme. Assisted-by: Claude Opus 5.5
100 lines
4.7 KiB
JavaScript
100 lines
4.7 KiB
JavaScript
// © 2026 TechnoLibre (http://www.technolibre.ca)
|
||
// License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
|
||
|
||
// La commande d'impression des épreuves du navigateur, côté node (§ 11.7,
|
||
// § 14.4) : vitest.config.js la déclare dans browser.commands, et la page
|
||
// l'appelle par commands de vitest/browser. Elle reçoit d'abord le contexte
|
||
// de la commande — la page Playwright, le cadre de l'épreuve — et rend du
|
||
// JSON, jamais d'octets : le RPC des commandes sérialiserait un tableau
|
||
// typé en objet {"0": …}.
|
||
//
|
||
// Le moteur d'impression de Chromium ne vise qu'une page entière, jamais le
|
||
// cadre où l'épreuve s'exécute. imprimerEnPdf sérialise donc le document du
|
||
// cadre — ses feuilles de style relues de leurs règles, ses scripts
|
||
// retirés, une base qui garde l'adresse des polices et des images — et
|
||
// l'imprime dans une page neuve du même contexte, sous les médias
|
||
// d'impression, à la taille de page que demande la feuille de style
|
||
// (preferCSSPageSize). La lecture du PDF (test/pdf.js, chargé à la première
|
||
// impression : la configuration du lanceur importe ce module sans lui)
|
||
// rend le nombre de pages, la boîte de média de chacune et sa couche de
|
||
// texte.
|
||
|
||
/** Les commandes que vitest.config.js déclare, toutes exportées ici. */
|
||
export const COMMANDES = Object.freeze(['imprimerEnPdf']);
|
||
|
||
/** L'écart admis entre la boîte de média d'un PDF de Chromium et le papier
|
||
* demandé, en mm : Chromium arrondit la taille de page à l'unité de son
|
||
* moteur, et l'A4 en sort à 209,89 × 297,01 mm. L'épreuve du navigateur
|
||
* et l'essai de démarrage la lisent toutes deux d'ici. */
|
||
export const TOLERANCE_PAGE_MM = 0.25;
|
||
|
||
/**
|
||
* Ce qu'un PDF imprimé donne à lire : le nombre de pages ; la boîte de
|
||
* média de chaque page, en mm ; le texte de chaque page, ses fragments
|
||
* dans l'ordre du flux, séparés d'une espace.
|
||
*
|
||
* @param {Uint8Array} octets
|
||
* @returns {Promise<{pages: number, mediaBox: Array<{largeur: number, hauteur: number}>, textes: string[]}>}
|
||
*/
|
||
export async function lireImpression(octets) {
|
||
const { lirePdf } = await import('../pdf.js');
|
||
const lu = await lirePdf(octets);
|
||
return {
|
||
pages: lu.pages.length,
|
||
mediaBox: lu.pages.map(({ boite }) => ({ largeur: boite.largeur, hauteur: boite.hauteur })),
|
||
textes: lu.pages.map(({ textes }) => textes.map(({ texte }) => texte).join(' ')),
|
||
};
|
||
}
|
||
|
||
// Le document du cadre, tel qu'une page neuve le rend sans rien exécuter,
|
||
// sous la forme d'un texte HTML. S'exécute dans le cadre : chaque feuille
|
||
// de style s'écrit de ses règles — un <style> qu'un module a rempli par
|
||
// l'objet CSSOM garde ainsi ce qu'il applique —, les feuilles adoptées en
|
||
// <style> à la fin de l'en-tête, aucun <script> ni préchargement de module
|
||
// ne reste, et une <base> en tête garde l'adresse du cadre pour les
|
||
// ressources relatives.
|
||
function serialiserDocument() {
|
||
const regles = (feuille) => [...feuille.cssRules].map((regle) => regle.cssText).join('\n');
|
||
const copie = document.documentElement.cloneNode(true);
|
||
const originaux = [...document.querySelectorAll('style')];
|
||
[...copie.querySelectorAll('style')].forEach((style, rang) => {
|
||
const feuille = originaux[rang]?.sheet;
|
||
if (feuille) style.textContent = regles(feuille);
|
||
});
|
||
for (const element of copie.querySelectorAll('script, link[rel="modulepreload"]')) element.remove();
|
||
const tete = copie.querySelector('head');
|
||
for (const feuille of document.adoptedStyleSheets) {
|
||
const style = document.createElement('style');
|
||
style.textContent = regles(feuille);
|
||
tete.append(style);
|
||
}
|
||
const base = document.createElement('base');
|
||
base.href = document.baseURI;
|
||
tete.prepend(base);
|
||
return `<!doctype html>\n${copie.outerHTML}`;
|
||
}
|
||
|
||
/**
|
||
* Imprime le document du cadre de l'épreuve en PDF, par le moteur
|
||
* d'impression de Chromium, dans une page neuve du même contexte, fermée
|
||
* ensuite : médias d'impression, fond imprimé, taille de page de la
|
||
* feuille de style. Les polices du document sont chargées avant
|
||
* l'impression. Le document de l'épreuve n'est pas modifié.
|
||
*
|
||
* @returns {Promise<{pages: number, mediaBox: Array<{largeur: number, hauteur: number}>, textes: string[]}>}
|
||
* lireImpression du PDF
|
||
*/
|
||
export async function imprimerEnPdf(context) {
|
||
const cadre = await context.frame();
|
||
const html = await cadre.evaluate(serialiserDocument);
|
||
const page = await context.page.context().newPage();
|
||
try {
|
||
await page.emulateMedia({ media: 'print' });
|
||
await page.setContent(html, { waitUntil: 'load' });
|
||
await page.evaluate(() => document.fonts.ready.then(() => undefined));
|
||
const octets = await page.pdf({ preferCSSPageSize: true, printBackground: true });
|
||
return await lireImpression(new Uint8Array(octets));
|
||
} finally {
|
||
await page.close();
|
||
}
|
||
}
|