// „Was wir zuletzt erledigt haben“ als Zeitleiste (Variante C): der DOM-Bau mit dem Fake-DOM. Die reine Logik prüft knAuftragsZeitleiste.test.js. import { test } from 'node:test' import assert from 'node:assert/strict' import { baueAktivitaet } from '../public/neu/auftrags-zeitleiste.js' import { mitDom } from './support/knDaten.js' const JETZT = new Date('2026-10-16T09:00:00Z') const ST = { neu: { label: 'Eingegangen', tone: 'new', step: 1 }, arbeit: { label: 'In Arbeit', tone: 'active', step: 2 }, wartet: { label: 'Wartet auf deine Rückmeldung', tone: 'waiting', step: 2 }, pruefung: { label: 'Wird geprüft', tone: 'review', step: 3 }, fertig: { label: 'Erledigt', tone: 'done', step: 4 }, pause: { label: 'Pausiert', tone: 'paused', step: 0 }, storniert: { label: 'Storniert', tone: 'stopped', step: 0 }, } const auftrag = (nr, st, createdAt, entries = [], topic = `Thema ${nr}`) => ({ number: String(nr), topic, status: ST[st], createdAt, entries }) const eintrag = (date, text, over = {}) => ({ kind: 'online', label: 'Online erledigt', date, minutes: 15, person: 'Lena Vogel', text, ...over }) /* ---------------------------------------------------------------------- DOM --- */ const dreiAuftraege = () => [ auftrag(100207, 'fertig', '2026-10-02T10:00:00Z', [eintrag('2026-10-04', 'Neue Öffnungszeiten eingetragen.')], 'Öffnungszeiten aktualisieren'), auftrag(100231, 'wartet', '2026-10-13T10:00:00Z', [eintrag('2026-10-15', 'Fotos erhalten.'), eintrag('2026-10-13', 'Bildauswahl vorbereitet.')], 'Neue Fotos für die Startseite'), auftrag(100224, 'pruefung', '2026-10-06T10:00:00Z', [ eintrag('2026-10-14', 'Speisekarte gesetzt.'), eintrag('2026-10-13', 'Layout geprüft.'), eintrag('2026-10-12', 'Entwurf erstellt.'), eintrag('2026-10-09', 'Texte gesammelt.'), ], 'Speisekarte als PDF'), ] test('Aktivität: ein Block je Auftrag, Titel als Überschrift, Pille in Farbe des Status, Reihenfolge laufend vor erledigt', () => mitDom((fx) => { const el = baueAktivitaet(dreiAuftraege(), JETZT) const bloecke = fx.alle(el, '[data-auftrag]') assert.deepEqual(bloecke.map((b) => b.getAttribute('data-auftrag')), ['100231', '100224', '100207']) const kopf = (b) => ({ titel: fx.finde(b, 'h3').textContent, pille: fx.finde(b, '.kn-pille').textContent, klasse: fx.finde(b, '.kn-pille').className }) assert.deepEqual(kopf(bloecke[0]), { titel: 'Neue Fotos für die Startseite', pille: 'Wartet auf deine Rückmeldung', klasse: 'kn-pille kn-pille-orange' }) assert.deepEqual(kopf(bloecke[1]), { titel: 'Speisekarte als PDF', pille: 'Wird geprüft', klasse: 'kn-pille kn-pille-violett' }) assert.deepEqual(kopf(bloecke[2]), { titel: 'Öffnungszeiten aktualisieren', pille: 'Erledigt', klasse: 'kn-pille kn-pille-gruen' }) assert.equal(fx.finde(bloecke[0], 'h3').className, 'kn-az-titel') })) test('Aktivität: In Arbeit ist blau, Eingegangen und Pausiert grau, Storniert rot', () => mitDom((fx) => { const el = baueAktivitaet([auftrag(1, 'arbeit', '2026-10-05T00:00:00Z'), auftrag(2, 'neu', '2026-10-04T00:00:00Z'), auftrag(3, 'pause', '2026-10-03T00:00:00Z'), auftrag(4, 'storniert', '2026-10-02T00:00:00Z')], JETZT) assert.deepEqual(fx.alle(el, '.kn-pille').map((p) => p.className.split(' ')[1]), ['kn-pille-blau', 'kn-pille-grau', 'kn-pille-grau', 'kn-pille-rot']) })) test('Aktivität: schmaler Balken mit vier Abschnitten, Stufennamen als Text für Screenreader', () => mitDom((fx) => { const el = baueAktivitaet(dreiAuftraege(), JETZT) const [wartet, pruefung, fertig] = fx.alle(el, '[data-auftrag]') const stand = (b) => fx.alle(fx.finde(b, '.kn-az-balken'), 'li').map((li) => [li.classList.contains('is-done'), li.getAttribute('aria-current')]) assert.deepEqual(stand(wartet), [[true, null], [true, 'step'], [false, null], [false, null]]) assert.deepEqual(stand(pruefung), [[true, null], [true, null], [true, 'step'], [false, null]]) assert.deepEqual(stand(fertig), [[true, null], [true, null], [true, null], [true, 'step']]) const balken = fx.finde(wartet, '.kn-az-balken'); assert.equal(balken.tagName, 'OL'); assert.equal(balken.getAttribute('aria-label'), 'Fortschritt des Auftrags') assert.deepEqual(fx.alle(balken, '.kn-nur-lesen').map((s) => s.textContent), ['Eingegangen, erreicht', 'In Arbeit, aktueller Stand', 'Prüfung, noch offen', 'Erledigt, noch offen']) assert.deepEqual(fx.alle(fertig, '.kn-nur-lesen').map((s) => s.textContent), ['Eingegangen, erreicht', 'In Arbeit, erreicht', 'Prüfung, erreicht', 'Erledigt, aktueller Stand']) })) test('Aktivität: Stufe 0 (pausiert, storniert) zeigt keinen Balken, nur die Pille', () => mitDom((fx) => { const el = baueAktivitaet([auftrag(1, 'pause', '2026-10-05T00:00:00Z'), auftrag(2, 'storniert', '2026-10-04T00:00:00Z')], JETZT) assert.equal(fx.alle(el, '[data-auftrag]').length, 2); assert.equal(fx.finde(el, '.kn-az-balken'), null) })) test('Aktivität: Rückfrage steht unter der Überschrift mit Link in den Chat, sonst keine Zeile', () => mitDom((fx) => { const el = baueAktivitaet(dreiAuftraege(), JETZT) const [wartet, pruefung, fertig] = fx.alle(el, '[data-auftrag]') const info = fx.finde(wartet, '.kn-az-info') assert.equal(info.textContent, 'Wir brauchen eine Info von dir · Im Chat antworten') const chat = fx.finde(info, 'a'); assert.deepEqual([chat.textContent, chat.getAttribute('href'), chat.getAttribute('data-view')], ['Im Chat antworten', '#chat', 'chat']) assert.equal(fx.alle(el, '.kn-az-info').length, 1); assert.equal(fx.finde(pruefung, '.kn-az-info'), null); assert.equal(fx.finde(fertig, '.kn-az-info'), null) // Reihenfolge im Block: Kopf, Info, Balken, Zeitleiste const klassen = [...wartet.children].map((k) => k.className.split(' ')[0]); assert.deepEqual(klassen, ['kn-az-kopf', 'kn-az-info', 'kn-az-balken', 'kn-az-zeitleiste']) })) test('Aktivität: Auftragsnummer erscheint nicht als Text (ruhiger), bleibt aber als Datenattribut', () => mitDom((fx) => { const el = baueAktivitaet(dreiAuftraege(), JETZT) assert.doesNotMatch(fx.text(el), /1002\d\d|Auftrag \d/) })) test('Aktivität: Zeitleiste mit Datum und Kurztext, drei sichtbar, „mehr anzeigen“ klappt den Rest auf', () => mitDom((fx) => { const el = baueAktivitaet(dreiAuftraege(), JETZT) const pruefung = fx.alle(el, '[data-auftrag]')[1] const zeilen = fx.alle(pruefung, '.kn-az-zeitleiste li') assert.deepEqual(zeilen.map((z) => fx.finde(z, 'time').textContent), ['14.10.', '13.10.', '12.10.', '09.10.']) assert.deepEqual(zeilen.map((z) => fx.finde(z, '.kn-az-text').textContent), ['Speisekarte gesetzt.', 'Layout geprüft.', 'Entwurf erstellt.', 'Texte gesammelt.']) assert.equal(fx.finde(zeilen[0], 'time').getAttribute('datetime'), '2026-10-14') assert.deepEqual(zeilen.map((z) => z.hasAttribute('data-extra')), [false, false, false, true]) const wrapper = fx.finde(pruefung, '[data-einklappbar]'); assert.ok(wrapper); assert.ok(!wrapper.classList.contains('is-open')) const mehr = fx.finde(pruefung, '[data-mehr]'); assert.equal(mehr.textContent, 'mehr anzeigen'); assert.equal(mehr.getAttribute('aria-expanded'), 'false') fx.klick(mehr) assert.ok(wrapper.classList.contains('is-open')); assert.equal(mehr.textContent, 'weniger anzeigen'); assert.equal(mehr.getAttribute('aria-expanded'), 'true') fx.klick(mehr) assert.ok(!wrapper.classList.contains('is-open')); assert.equal(mehr.textContent, 'mehr anzeigen'); assert.equal(mehr.getAttribute('aria-expanded'), 'false') // genau drei Zeilen: kein Knopf; weniger: kein Knopf assert.equal(fx.finde(fx.alle(el, '[data-auftrag]')[0], '[data-mehr]'), null) assert.equal(fx.alle(el, '[data-mehr]').length, 1) })) test('Aktivität: Auftrag ohne Einträge zeigt einen ruhigen Satz statt einer leeren Zeitleiste', () => mitDom((fx) => { const el = baueAktivitaet([auftrag(1, 'arbeit', '2026-10-05T00:00:00Z', [])], JETZT) assert.equal(fx.finde(el, '.kn-az-zeitleiste'), null); assert.match(fx.text(el), /Noch keine Arbeiten dokumentiert\./) })) test('Aktivität: fünf Aufträge sichtbar, „Ältere Aufträge anzeigen“ klappt den Rest auf', () => mitDom((fx) => { const liste = Array.from({ length: 8 }, (_, i) => auftrag(200 + i, i === 7 ? 'fertig' : 'arbeit', `2026-10-0${i + 1}T00:00:00Z`, [eintrag(`2026-10-0${i + 1}`, `Eintrag ${i}`)])) const el = baueAktivitaet(liste, JETZT) const bloecke = fx.alle(el, '[data-auftrag]') assert.equal(bloecke.length, 8) assert.deepEqual(bloecke.map((b) => b.hasAttribute('data-extra')), [false, false, false, false, false, true, true, true]) assert.deepEqual(bloecke.map((b) => b.getAttribute('data-auftrag')), ['206', '205', '204', '203', '202', '201', '200', '207']) const alt = fx.finde(el, '[data-aeltere]'); assert.equal(alt.textContent, 'Ältere Aufträge anzeigen'); assert.equal(alt.getAttribute('aria-expanded'), 'false') const huelle = alt.closest('[data-einklappbar]'); assert.ok(!huelle.classList.contains('is-open')) fx.klick(alt); assert.ok(huelle.classList.contains('is-open')); assert.equal(alt.textContent, 'Weniger anzeigen'); assert.equal(alt.getAttribute('aria-expanded'), 'true') fx.klick(alt); assert.ok(!huelle.classList.contains('is-open')); assert.equal(alt.textContent, 'Ältere Aufträge anzeigen') assert.equal(fx.finde(baueAktivitaet(liste.slice(0, 5), JETZT), '[data-aeltere]'), null) })) test('Aktivität: Zeilen-Knopf und Aufträge-Knopf sind getrennt (mehr anzeigen öffnet nicht die älteren Aufträge)', () => mitDom((fx) => { const viele = Array.from({ length: 6 }, (_, i) => auftrag(300 + i, 'arbeit', `2026-10-0${i + 1}T00:00:00Z`, ['a', 'b', 'c', 'd'].map((t, j) => eintrag(`2026-10-1${j}`, t)))) const el = baueAktivitaet(viele, JETZT) const huelle = fx.finde(el, '[data-aeltere]').closest('[data-einklappbar]') const ersterMehr = fx.finde(fx.alle(el, '[data-auftrag]')[0], '[data-mehr]') fx.klick(ersterMehr); assert.ok(!huelle.classList.contains('is-open')); assert.ok(ersterMehr.closest('[data-einklappbar]').classList.contains('is-open')) })) test('Aktivität: fremde Inhalte bleiben Text; leer und kaputte Elemente', () => mitDom((fx) => { const boese = '' const el = baueAktivitaet([auftrag(1, 'arbeit', '2026-10-05T00:00:00Z', [eintrag('2026-10-06', boese), eintrag('2026-10-05', 'x', { changes: [boese] })], boese)], JETZT) assert.equal(fx.finde(el, 'img'), null); assert.ok(fx.text(el).includes(boese)) assert.equal(baueAktivitaet([], JETZT).textContent, 'Es wurden noch keine Arbeiten dokumentiert.') assert.equal(baueAktivitaet(undefined, JETZT).textContent, 'Es wurden noch keine Arbeiten dokumentiert.') const roh = baueAktivitaet([{ number: '9' }, { topic: 'Nur Titel', status: null, entries: null }], JETZT) assert.equal(fx.alle(roh, '.kn-az').length, 2); assert.match(fx.text(roh), /Dein Auftrag/); assert.doesNotMatch(fx.text(roh), /null|undefined/) }))