Je Auftrag eine Überschrift (Titel fett, Status-Pille), ein schmaler Balken mit vier Abschnitten (Stufennamen als Text für Screenreader), bei Rückfrage die Zeile "Wir brauchen eine Info von dir · Im Chat antworten" und eine Zeitleiste mit Haarlinie (Datum TT.MM. und Kurztext, drei sichtbar, "mehr anzeigen"). Fünf Aufträge offen, der Rest hinter "Ältere Aufträge anzeigen". Die Auftragsnummer steht nicht mehr im Text (ruhiger), bleibt als data-auftrag. Die alte baueAktivitaet und ihre toten CSS-Regeln entfallen; die Projektdetails (baueAuftrag, baueArbeiten) bleiben unverändert. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
134 lines
11 KiB
JavaScript
134 lines
11 KiB
JavaScript
// „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 = '<img src=x onerror=alert(1)>'
|
|
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/)
|
|
}))
|