← Files JUJIES ArbeitsblätterARCHIVED FILE

tests/check-finalize.mjs

8.6 KB · Nov 11, 2026 · 13:26 UTC

↓ Download file

import {mkdtemp,mkdir,writeFile,readFile,rm,access,symlink} from 'node:fs/promises';
import path from 'node:path';
import assert from 'node:assert/strict';
import {finalizeMaterial,compareScreenPages} from '../skills/arbeitsblatt-html/scripts/finalize-material.mjs';

const px=mm=>mm*96/25.4;
const margins={top:12,right:14,bottom:12,left:14};
const screen={width:px(210),height:px(297),padding:Object.fromEntries(Object.entries(margins).map(([k,v])=>[k,px(v)]))};
const print={paperWidthMm:210,paperHeightMm:297,marginsMm:margins};
assert(compareScreenPages([screen],[print]).ok);
assert(!compareScreenPages([{...screen,width:px(182)}],[print]).ok,'Content-width screen page must fail');
assert(!compareScreenPages([{...screen,padding:{...screen.padding,left:0}}],[print]).ok,'Mismatched screen margin must fail');
assert(!compareScreenPages([{...screen,height:px(200)}],[print]).ok);
assert(!compareScreenPages([screen],[{...print,paperWidthMm:216,paperHeightMm:279}]).ok);
assert(!compareScreenPages([{...screen,width:NaN}],[print]).ok,'Unresolved dimensions must not pass');
assert(compareScreenPages([{...screen,width:px(297),height:px(210)}],[{...print,paperWidthMm:297,paperHeightMm:210}]).ok);
if(process.argv.includes('--geometry-only')) {console.log(JSON.stringify({ok:true,screenGeometryCases:7,browserIntegration:'not-run'}));process.exit(0);}

const scratch=await mkdtemp(path.resolve('work/finalize-test-'));
const source=path.join(scratch,'probe');
const output=path.join(scratch,'ausgabe');
const playwrightModule=process.env.AB_PLAYWRIGHT_MODULE;
await mkdir(source);
const html=answer=>`<!doctype html><html lang="de"><head><meta charset="utf-8"><title>Probe</title><link rel="stylesheet" href="style.css"></head><body class="${answer?'solutions':'student'}"><section data-tafelraum-page><h1>Testbogen</h1><label for="answer">Erkläre den Weg.</label><textarea id="answer">${answer}</textarea><label><input type="checkbox" id="wahl">Versickerung</label></section></body></html>`;
const css=`*{box-sizing:border-box}body{margin:0;font:12pt Arial}section{width:210mm;min-height:297mm;padding:15mm}textarea{display:block;width:100%;height:30mm;resize:vertical;font:inherit}input{appearance:none;width:5mm;height:5mm;border:1px solid black}input:checked{background:black}@page{size:A4;margin:15mm}@media print{section{width:auto;min-height:0;padding:0}textarea{height:30mm!important;min-height:30mm!important;max-height:30mm!important;width:100%!important}body.student textarea{color:transparent!important;-webkit-text-fill-color:transparent!important}input:checked{background:transparent!important}}`;
await writeFile(source+'/manifest.json',JSON.stringify({format:'tafelraum-interactive-html',schemaVersion:1,id:'probe',title:'Probe',entry:'index.html',defaultViewport:{width:794,height:1123},print:{entry:'index.html',page:'A4',orientation:'portrait',mode:'template',layout:'shared-pages'}}));
await writeFile(source+'/index.html',html(''));
await writeFile(source+'/style.css',css);
const run=(role='student',extra={})=>finalizeMaterial({sourceDirectory:source,outputDirectory:output,role,playwrightModule,browserExecutable:process.env.AB_BROWSER_EXECUTABLE,...extra});
try {
 const original=await readFile(source+'/style.css');
 let result=await run();
 assert.equal(result.status,'technical_pass',JSON.stringify(result.errors));
 assert.equal(result.checks.print.pixelIdenticalAfterInput,true);
 assert.equal(result.checks.print.noExtractableTestAnswers,true);
 assert.equal(result.checks.print.pages,1);
 assert.equal(result.checks.browserStart.reviewToggle,true);
 assert.equal(result.checks.browserStart.answersRetained,true);
 await access(output+'/TECHNIK_OK');
 assert.deepEqual(await readFile(source+'/style.css'),original,'Source was changed');
 const start=await readFile(result.browserStart,'utf8');
 assert(start.includes('ab-browser-print'));
 // The actual print DOM catches visible answer shading, including a tight wrapper.
 await writeFile(source+'/style.css',css+'textarea{background:repeating-linear-gradient(white 0 6mm,#bcc 6mm 7mm)}');
 result=await run();assert.equal(result.status,'failed');assert.equal(result.checks.answerSurfaces.ok,false);
 assert.match(result.errors.join(),/CSS-Verlauf.*Antwortfläche/);
 await assert.rejects(access(output+'/TECHNIK_OK'));
 await writeFile(source+'/style.css',css+'.unsafe{height:30mm;background:linear-gradient(white,#bcc)}.unsafe textarea{background:transparent}');
 await writeFile(source+'/index.html',html('').replace('<textarea','<div class="unsafe"><textarea').replace('</textarea>','</textarea></div>'));
 result=await run();assert.equal(result.status,'failed');assert.equal(result.checks.answerSurfaces.unsafe[0].owner,'wrapper');
 await writeFile(source+'/index.html',html(''));
 // Decoration and screen-only patterns do not make a clean PDF fail.
 await writeFile(source+'/style.css',css+'h1{background:linear-gradient(white,#ddd)}textarea{background:linear-gradient(white,#ddd)}@media print{textarea{background:white}}');
 result=await run();assert.equal(result.status,'technical_pass');assert.equal(result.checks.answerSurfaces.ok,true);
 await writeFile(source+'/style.css',css);
 // A real 2 → 1 page revision in the same destination clears obsolete rendered pages.
 await writeFile(source+'/index.html',html('').replace('</body>','<section data-tafelraum-page style="break-before:page"><h2>Zweite Seite</h2></section></body>'));
 result=await run();assert.equal(result.status,'technical_pass');assert.equal(result.printImages.length,2);
 await writeFile(source+'/index.html',html(''));
 result=await run();assert.equal(result.status,'technical_pass');assert.equal(result.printImages.length,1);
 // A growing min-height page hides overflow from a border-box-only check.
 await writeFile(source+'/index.html',html('').replace('</section>','<div style="height:280mm">Langer Inhalt</div></section>'));
 result=await run();assert.equal(result.status,'failed');
 assert(result.checks.pagination.pages[0].overflowMm>20);
 assert.equal(result.checks.pagination.pages[0].availableHeightMm,267);
 assert(result.checks.pagination.actualPages>1);
 assert.match(result.errors.join(),/Seite 1.*Druckhöhe/);
 await assert.rejects(access(output+'/TECHNIK_OK'));
 // A fixed page can clip descendants even when its own height fits.
 await writeFile(source+'/style.css',css+'@media print{section{height:250mm;overflow:hidden}}');
 result=await run();assert.equal(result.status,'failed');assert(result.checks.pagination.pages[0].outside.length);
 await writeFile(source+'/style.css',css);
 await writeFile(source+'/index.html',html(''));
 // Same valid source contract, real missing resize override: must reject.
 await writeFile(source+'/style.css',css.replaceAll('!important',''));
 result=await run();assert.equal(result.status,'failed');
 assert(result.checks.print.changedGeometry.length>0);
 await assert.rejects(access(output+'/TECHNIK_OK'));
 await writeFile(source+'/style.css',css);
 // The central print lifecycle, rather than transparent ink, owns blanking.
 await writeFile(source+'/style.css',css.replace('color:transparent!important;-webkit-text-fill-color:transparent!important','color:black'));
 result=await run();assert.equal(result.status,'technical_pass');assert.equal(result.checks.print.noExtractableTestAnswers,true);
 await writeFile(source+'/style.css',css.replace('input:checked{background:transparent!important}',''));
 result=await run();assert.equal(result.status,'technical_pass');assert.equal(result.checks.print.pixelIdenticalAfterInput,true);
 await writeFile(source+'/style.css',css);
 // Intentional solution answers survive: no blanket blanking in the renderer.
 await writeFile(source+'/index.html',html('Wasser versickert im Boden.'));
 result=await run('solutions');assert.equal(result.status,'technical_pass',JSON.stringify(result.errors));
 result=await run('student');assert.equal(result.status,'failed');assert.match(result.errors.join(),/ausgefüllte/);
 await writeFile(source+'/index.html',html(''));
 result=await run('student',{playwrightModule:path.join(scratch,'missing-runtime')});assert.equal(result.status,'failed');
 await assert.rejects(run('student',{outputDirectory:source+'/ausgabe'}),/getrennte/);
 await symlink(source,path.join(scratch,'alias'));
 await assert.rejects(run('student',{outputDirectory:path.join(scratch,'alias','ausgabe')}),/getrennte/);
 console.log(JSON.stringify({ok:true,positive:7,negative:9,sourceUnchanged:true,studentBlank:true,noExtractableTestAnswers:true,resizeRegression:true,solutionsRetained:true,missingRuntimeFails:true,sourceAndSymlinkProtected:true,outputReuseAfterPageReduction:true,gradientFieldAndWrapperRejected:true,printOverrideAndDecorationAllowed:true}));
}finally {await rm(scratch,{recursive:true,force:true});}

SHA-256: c6aa8b5d8b9adf8ab28b586845cab68b312a380282609eced185a4847f29eb09