135 lines
7.3 KiB
HTML
135 lines
7.3 KiB
HTML
<!doctype html>
|
|
<!--
|
|
@file ui_smoke.html
|
|
@brief UI-Rauchtest: bedient index.html im iframe (Start, Antwort, Auflösung, Speichern, Prüfungsabgabe)
|
|
@author Björn Runden <br@runden.de>
|
|
@owner Björn Runden <br@runden.de>
|
|
@assist KI-unterstützt (Claude Opus 5.5), geprüft von Björn Runden <br@runden.de>
|
|
@version 0.1.0 (29.10.2026)
|
|
@usage chromium --headless --allow-file-access-from-files --virtual-time-budget=20000 --dump-dom tests/ui_smoke.html
|
|
?stop=learn|exam|result hält an dieser Stelle an (für Screenshots)
|
|
-->
|
|
<html lang="de">
|
|
<head><meta charset="utf-8"><title>UI-Rauchtest</title>
|
|
<style>html,body{margin:0;height:100%;background:#282a36}iframe{border:0;width:100%;height:100%}#out{position:fixed;left:0;bottom:0;color:#fff;font:11px monospace;max-height:30%;overflow:auto;background:#000a;display:none}</style>
|
|
</head>
|
|
<body>
|
|
<iframe id="app" src="../index.html"></iframe>
|
|
<pre id="out"></pre>
|
|
<script>
|
|
(async function () {
|
|
'use strict';
|
|
const stop = new URLSearchParams(location.search).get('stop');
|
|
const out = document.getElementById('out');
|
|
let pass = 0, fail = 0;
|
|
const t = (name, cond) => { out.textContent += `${cond ? 'ok ' : 'FAIL'} ${name}\n`; cond ? pass++ : fail++; };
|
|
const wait = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
const done = () => { out.textContent += `RESULT pass=${pass} fail=${fail}\n`; if (!stop) out.style.display = 'block'; };
|
|
|
|
try {
|
|
const frame = document.getElementById('app');
|
|
await new Promise((r) => frame.addEventListener('load', r));
|
|
const w = frame.contentWindow, d = w.document;
|
|
w.localStorage.clear();
|
|
await wait(800);
|
|
const $ = (s) => d.querySelector(s);
|
|
|
|
t('Startseite sichtbar', !!$('#setup'));
|
|
// Lernmodus, nach Objective sortiert, ungemischt
|
|
$('#setup [name=order]').value = 'objective';
|
|
$('#setup [name=shuffle]').checked = false;
|
|
$('#setup [name=count]').value = '3';
|
|
$('#setup [type=submit]').click();
|
|
await wait(500);
|
|
const sec = () => $('section.present');
|
|
t('Frage 1 angezeigt', sec() && sec().classList.contains('question'));
|
|
const q = w.LPICSim.ui.session.questions[0];
|
|
const correct = q.options.find((o) => o.correct).id;
|
|
sec().querySelector(`input[value="${correct}"]`).click();
|
|
await wait(100);
|
|
t('Antwort gespeichert (localStorage)', JSON.parse(w.localStorage.getItem('lpic_sim.session')).answers[q.id].value[0] === correct);
|
|
t('Lösung noch verborgen', !sec().querySelector('.solution').classList.contains('visible'));
|
|
if (stop === 'learn') return done();
|
|
w.Reveal.next();
|
|
await wait(400);
|
|
t('noch auf Frage 1 (Auflösung statt Weiter)', Number(sec().dataset.index) === 0);
|
|
t('Auflösung sichtbar und kein Fragment mehr', !sec().querySelector('.solution').classList.contains('fragment'));
|
|
t('Urteil „Richtig“', sec().querySelector('.verdict.right') !== null);
|
|
t('Eingaben gesperrt', [...sec().querySelectorAll('input')].every((i) => i.disabled));
|
|
t('Option grün markiert', sec().querySelector('.option.is-correct') !== null);
|
|
if (stop === 'solution') return done();
|
|
w.Reveal.next();
|
|
await wait(400);
|
|
t('Frage 2 nach zweitem Weiter', Number(sec().dataset.index) === 1);
|
|
w.Reveal.next(); await wait(300); // Frage 2 unbeantwortet auflösen
|
|
t('unbeantwortet = „Nicht beantwortet“', sec().querySelector('.verdict.skipped') !== null);
|
|
w.Reveal.prev(); await wait(300);
|
|
t('Zurück auf Frage 1, Auflösung bleibt', Number(sec().dataset.index) === 0 && sec().querySelector('.verdict.right') !== null);
|
|
|
|
// Menü → gespeicherte Sitzung → fortsetzen
|
|
d.querySelector('[data-act=menu]').click(); await wait(400);
|
|
t('Startseite bietet Fortsetzen', !!$('.resume'));
|
|
$('.resume [data-act=resume]').click(); await wait(400);
|
|
t('Fortgesetzt an gespeicherter Stelle', Number(sec().dataset.index) === 0);
|
|
|
|
// Prüfungsmodus
|
|
d.querySelector('[data-act=menu]').click(); await wait(400);
|
|
$('#setup [name=mode][value=exam]').click(); await wait(100);
|
|
$('#setup [type=submit]').click(); await wait(500);
|
|
const s = w.LPICSim.ui.session;
|
|
t('Prüfungsmodus aktiv', s.mode === 'exam' && s.time_limit_s === 5400);
|
|
t('keine Auflösung im Prüfungsmodus', !sec().querySelector('.solution'));
|
|
t('Timer läuft rückwärts', /⏱ (1:29|1:30):/.test(d.getElementById('tb-timer').textContent));
|
|
const fill = sec().querySelector('input.fill');
|
|
if (fill) { fill.value = 'test'; fill.dispatchEvent(new w.Event('input', { bubbles: true })); }
|
|
else sec().querySelector('.options input').click();
|
|
sec().querySelector('[data-flag]').click();
|
|
await wait(100);
|
|
t('Markierung gespeichert', s.answers[s.questions[0].id].flagged === true);
|
|
if (stop === 'exam') return done();
|
|
w.Reveal.slide(s.questions.length); await wait(400);
|
|
t('Abgabe-Folie', sec().classList.contains('submit'));
|
|
w.confirm = () => true;
|
|
sec().querySelector('[data-act=submit]').click(); await wait(600);
|
|
t('abgegeben, Ergebnis-Folie', s.submitted && sec().classList.contains('result'));
|
|
t('Ergebnis zeigt Quote', /von \d+ richtig/.test(sec().textContent));
|
|
w.Reveal.slide(0); await wait(400);
|
|
t('nach Abgabe Auflösung sichtbar', !!sec().querySelector('.solution .verdict'));
|
|
if (stop === 'result') { w.Reveal.slide(s.questions.length); await wait(400); return done(); }
|
|
|
|
// Import über das Datei-Feld: erst Fragenpaket, dann exportierte Sitzung
|
|
const importJson = async (name, obj) => {
|
|
const dt = new w.DataTransfer();
|
|
dt.items.add(new w.File([JSON.stringify(obj)], name, { type: 'application/json' }));
|
|
const input = d.getElementById('import-file');
|
|
input.files = dt.files;
|
|
const n = d.querySelectorAll('.toast').length;
|
|
input.dispatchEvent(new w.Event('change', { bubbles: true }));
|
|
// FileReader arbeitet asynchron: auf die Rückmeldung (Toast) warten, höchstens 5 s
|
|
for (let i = 0; i < 50 && d.querySelectorAll('.toast').length === n; i++) await wait(100);
|
|
await wait(300);
|
|
};
|
|
d.querySelector('[data-act=menu]').click(); await wait(400);
|
|
const exam101 = w.LPICSim.ui.exams.find((e) => e.id === 'lpic1-101');
|
|
const before = exam101.bank.length;
|
|
const extra = { id: '101.1-990', objective: '101.1', type: 'fill', question: 'Testfrage', explanation: 'x', answers: ['ok'] };
|
|
await importJson('paket.json', { format: 'lpic-sim-questions', exam: 'lpic1-101', questions: [extra, { ...extra, id: 'kaputt' }] });
|
|
t('Paket-Import: 1 Frage ergänzt, 1 abgelehnt', exam101.bank.length === before + 1);
|
|
t('Paket in localStorage gemerkt', (JSON.parse(w.localStorage.getItem('lpic_sim.packs')) || []).length === 1);
|
|
t('Startseite zählt neue Frage', $('#setup').textContent.includes(`${before + 1} Fragen`));
|
|
const saved = JSON.parse(JSON.stringify(s));
|
|
saved.submitted = false; saved.current = 2; saved.answers = {};
|
|
await importJson('sitzung.json', saved);
|
|
t('Sitzungs-Import setzt an Frage 3 fort', Number(sec().dataset.index) === 2 && w.LPICSim.ui.session.mode === 'exam');
|
|
await importJson('kaputt.json', { format: 'unbekannt' });
|
|
t('Unbekanntes Format: Fehlermeldung', d.querySelector('.toast.error') !== null);
|
|
w.localStorage.removeItem('lpic_sim.packs');
|
|
} catch (e) {
|
|
t(`Ausnahme: ${e && e.stack || e}`, false);
|
|
}
|
|
done();
|
|
})();
|
|
</script>
|
|
</body>
|
|
</html>
|