Files
Webklar-Kundenbereich/test/knAuftragsZeitleisteDom.test.js
T
ClaudeandClaude Sonnet 5.5 f3f8ac3777 Kundenansicht neu: doppelte Klasse .kn-nur-lesen (ansichten.css) durch die vorhandene .kn-nur-leser (huelle.css) ersetzt
Die Stufennamen im Fortschrittsbalken der Zeitleiste (nur für Vorlesegeräte) tragen jetzt .kn-nur-leser; die zweite,
gleichwertige Regel .kn-nur-lesen in ansichten.css ist gestrichen. Tests: DOM- und Quelltext-Test (keine Spur von
.kn-nur-lesen in Stil und Modul), Stil-Test sucht die Klassenregeln auch in huelle.css.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-17 15:48:56 +00:00

233 lines
20 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 { readFileSync } from 'node:fs'
import { baueAktivitaet } from '../public/neu/auftrags-zeitleiste.js'
import { mitDom } from './support/knDaten.js'
import { customerOrder, customerWorkEntries } from '../server/services/customerProjectView.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-leser').map((s) => s.textContent), ['Eingegangen, erreicht', 'In Arbeit, aktueller Stand', 'Prüfung, noch offen', 'Erledigt, noch offen'])
assert.deepEqual(fx.alle(fertig, '.kn-nur-leser').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')
// Die ganze Zeile ist der Link in den Chat (eine große Tippfläche am Handy); nur „Im Chat antworten“ ist unterstrichen
const chat = fx.finde(info, 'a'); assert.deepEqual([chat.textContent, chat.getAttribute('href'), chat.getAttribute('data-view')], ['Wir brauchen eine Info von dir · Im Chat antworten', '#chat', 'chat'])
assert.equal(fx.finde(chat, '.kn-az-info-link').textContent, 'Im Chat antworten'); assert.equal(fx.alle(info, 'a').length, 1)
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)
}))
// Eine Klasse für Text nur für Vorlesegeräte: die vorhandene .kn-nur-leser (huelle.css). Die zweite, .kn-nur-lesen (ansichten.css), ist entfallen.
test('Nur für Vorlesegeräte: die Zeitleiste nutzt .kn-nur-leser aus huelle.css, .kn-nur-lesen gibt es in keinem Stil und keiner Ansicht mehr', () => mitDom((fx) => {
const css = (name) => readFileSync(new URL(`../public/neu/${name}`, import.meta.url), 'utf8')
assert.match(css('huelle.css'), /\n\.kn-nur-leser \{[^}]*position: absolute;[^}]*\}/)
assert.doesNotMatch(css('ansichten.css'), /kn-nur-lesen\b/)
assert.doesNotMatch(readFileSync(new URL('../public/neu/auftrags-zeitleiste.js', import.meta.url), 'utf8'), /kn-nur-lesen\b/)
const balken = fx.finde(baueAktivitaet(dreiAuftraege(), JETZT), '.kn-az-balken')
assert.equal(fx.alle(balken, 'li > .kn-nur-leser').length, 4)
assert.equal(fx.alle(balken, '.kn-nur-lesen').length, 0)
}))
// Barrierefreiheit: Mehrere Knöpfe „mehr anzeigen“ auf einer Seite sind für Screenreader nicht zu unterscheiden. Der Name nennt darum den Auftrag (und enthält den sichtbaren
// Text, WCAG 2.5.3), aria-controls zeigt auf die Liste, die der Knopf aufklappt.
test('Zeitleiste: „mehr anzeigen“ trägt aria-label mit dem Auftragstitel und aria-controls auf die eigene Liste, auch nach dem Umschalten', () => mitDom((fx) => {
const vier = (nr, titel) => auftrag(nr, 'arbeit', '2026-10-06T10:00:00Z', ['a', 'b', 'c', 'd'].map((t, j) => eintrag(`2026-10-1${j}`, t)), titel)
const el = baueAktivitaet([vier(1, 'Speisekarte als PDF'), vier(2, 'Neue Fotos <b>für</b> die Startseite')], JETZT)
const [erster, zweiter] = fx.alle(el, '[data-auftrag]')
const knopf = (block) => fx.finde(block, '[data-mehr]')
const liste = (block) => fx.finde(block, '.kn-az-zeilen')
assert.equal(knopf(erster).textContent, 'mehr anzeigen')
assert.equal(knopf(erster).getAttribute('aria-label'), 'mehr anzeigen zu „Speisekarte als PDF“')
assert.equal(knopf(zweiter).getAttribute('aria-label'), 'mehr anzeigen zu „Neue Fotos <b>für</b> die Startseite“') // nur Text, nie HTML
// aria-controls: die Id der Liste desselben Auftrags, eindeutig auf der Seite
const ids = [liste(erster).getAttribute('id'), liste(zweiter).getAttribute('id')]
assert.ok(ids.every((id) => /^[A-Za-z][\w-]*$/.test(id)) && ids[0] !== ids[1], ids.join())
assert.deepEqual([knopf(erster), knopf(zweiter)].map((k) => k.getAttribute('aria-controls')), ids)
assert.ok(knopf(erster).getAttribute('aria-label').startsWith(knopf(erster).textContent)) // der sichtbare Text steht im Namen
// Umschalten: Text, Zustand und Name ziehen mit, die Verknüpfung bleibt
fx.klick(knopf(erster))
assert.equal(knopf(erster).textContent, 'weniger anzeigen')
assert.equal(knopf(erster).getAttribute('aria-label'), 'weniger anzeigen zu „Speisekarte als PDF“')
assert.equal(knopf(erster).getAttribute('aria-controls'), ids[0])
assert.equal(knopf(zweiter).getAttribute('aria-label'), 'mehr anzeigen zu „Neue Fotos <b>für</b> die Startseite“') // der andere Knopf bleibt
fx.klick(knopf(erster))
assert.equal(knopf(erster).getAttribute('aria-label'), 'mehr anzeigen zu „Speisekarte als PDF“')
// ohne Titel gilt der Ersatztitel der Überschrift; „Ältere Aufträge anzeigen“ bleibt wie es war
const ohneTitel = baueAktivitaet([{ ...vier(3, ''), topic: '' }], JETZT)
assert.equal(fx.finde(ohneTitel, '[data-mehr]').getAttribute('aria-label'), 'mehr anzeigen zu „Dein Auftrag“')
const sechs = baueAktivitaet(Array.from({ length: 6 }, (_, i) => vier(10 + i, `T${i}`)), JETZT)
assert.equal(fx.finde(sechs, '[data-aeltere]').getAttribute('aria-label'), null)
}))
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/)
}))
// Vertrag mit dem Server: GET /api/activity baut jeden Auftrag aus customerOrder() und customerWorkEntries() (server/routes/customerData.js).
// Hier läuft die echte Server-Aufbereitung in die Zeitleiste, damit eine geänderte Antwortform den Test rot macht.
test('Vertrag: Aufträge in der Form von /api/activity (customerOrder, customerWorkEntries) erscheinen mit Datum und Kurztext', () => mitDom((fx) => {
const ticket = (woid, status, createdAt, worksheets) => ({ woid, topic: `Thema ${woid}`, status, createdAt, worksheets })
const antwort = [
ticket('100231', 'awaiting', '2026-10-13T10:00:00.000Z', [
{ serviceType: 'REMOTE', date: '14.10.2026', totalTime: 50, employeeName: 'Lena Vogel', details: 'Fotos erhalten und gesichtet.\nWeitere Zeile.', supporters: '' },
{ serviceType: 'GIT', date: '2026-10-12', totalTime: 0, isComment: true, details: '- [abcdef1](https://git.example/x/commit/abcdef1): feat: Menü angepasst (Kenso)\n- [abcdef2](https://git.example/x/commit/abcdef2): Bilder getauscht (Kenso)' },
]),
ticket('100207', 'closed', '2026-10-02T10:00:00.000Z', [{ serviceType: 'ON SITE', date: '04.10.2026', totalTime: 60, employeeName: 'Kenso', details: '' }]),
].map((t) => ({ ...customerOrder(t), createdAt: t.createdAt, entries: customerWorkEntries(t.worksheets) }))
const el = baueAktivitaet(antwort, JETZT)
const [wartet, fertig] = fx.alle(el, '[data-auftrag]')
assert.deepEqual([wartet.getAttribute('data-auftrag'), fertig.getAttribute('data-auftrag')], ['100231', '100207'])
assert.equal(fx.finde(wartet, '.kn-pille').textContent, 'Wartet auf deine Rückmeldung'); assert.ok(fx.finde(wartet, '.kn-az-info'))
assert.deepEqual(fx.alle(wartet, '.kn-az-zeilen li').map((z) => z.textContent), ['14.10.Fotos erhalten und gesichtet.', '12.10.Menü angepasst und 1 weitere Änderung'])
assert.equal(fx.finde(fertig, '.kn-pille').textContent, 'Erledigt'); assert.deepEqual(fx.alle(fertig, '.kn-az-zeilen li').map((z) => z.textContent), ['04.10.Vor Ort bei dir'])
}))
// Regression (Browserlauf 17.10.): Die allgemeine Regel `.kn-einklappbar:not(.is-open) [data-extra]` trifft jeden Nachfahren. Läge sie an den verschachtelten Wrappern
// (Aufträge, darin die Zeitleisten), verbärge ein zugeklappter äußerer Wrapper auch die Zeilen einer aufgeklappten Zeitleiste. Darum tragen beide Wrapper eigene Klassen
// und eigene Regeln nur für direkte Kinder.
test('Einklappen: die Wrapper verschachteln sich nicht über .kn-einklappbar, die Regeln gelten nur für direkte Kinder', () => mitDom((fx) => {
const viele = Array.from({ length: 6 }, (_, i) => auftrag(400 + 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)
assert.deepEqual(fx.alle(el, '[data-einklappbar]').map((w) => w.classList.contains('kn-einklappbar')), new Array(7).fill(false))
assert.equal(el.classList.contains('kn-az-liste'), true); assert.equal(fx.finde(el, '[data-einklappbar] [data-einklappbar]').classList.contains('kn-az-zeitleiste'), true)
const css = readFileSync(new URL('../public/neu/ansichten.css', import.meta.url), 'utf8')
assert.match(css, /\.kn-az-liste:not\(\.is-open\) > \.kn-az-auftraege > \[data-extra\] \{ display: none; \}/)
assert.match(css, /\.kn-az-zeitleiste:not\(\.is-open\) > \.kn-az-zeilen > \[data-extra\] \{ display: none; \}/)
}))
// Browserlauf 17.10.: Ein display:flex am Link zerlegte den Satz in zwei Spalten (Text | „Im Chat antworten“). Der Link ist ein Block; die Tippfläche kommt aus dem Innenabstand.
test('Rückfrage-Link: am Handy ein Block (kein flex) mit Innenabstand für 44 px Tippfläche', () => {
const css = readFileSync(new URL('../public/neu/ansichten.css', import.meta.url), 'utf8')
assert.match(css, /\n \.kn-az-info a \{ display: block; padding: 12px 0; \}/)
assert.doesNotMatch(css, /\.kn-az-info a \{[^}]*display: *(inline-)?flex/)
})
// Browserlauf 17.10. (Prüfbefund): „Im Chat antworten“ brach am schmalen Handy mitten im Text um. Der Linktext bleibt zusammen; passt er nicht mehr in die Zeile, wandert er ganz in die nächste.
test('Rückfrage-Link: der Linktext „Im Chat antworten“ bricht nicht um (white-space: nowrap auf .kn-az-info-link)', () => {
const css = readFileSync(new URL('../public/neu/ansichten.css', import.meta.url), 'utf8')
assert.match(css, /\n\.kn-az-info-link \{[^}]*\bwhite-space: nowrap;[^}]*\}/)
assert.doesNotMatch(css, /\.kn-az-info a \{[^}]*white-space: nowrap/) // der Satz davor darf umbrechen
})
test('Zeitleiste: trägt ein Eintrag das Jahr (Vorjahr), richten sich alle Datumsspalten des Auftrags danach', () => mitDom((fx) => {
const mitJahr = baueAktivitaet([auftrag(1, 'arbeit', '2026-10-05T00:00:00Z', [eintrag('2026-10-06', 'neu'), eintrag('2025-12-24', 'alt')])], JETZT)
assert.deepEqual(fx.alle(mitJahr, 'time').map((t) => t.textContent), ['06.10.', '24.12.25']); assert.ok(fx.finde(mitJahr, '.kn-az-zeilen').classList.contains('kn-az-mit-jahr'))
const ohne = baueAktivitaet([auftrag(2, 'arbeit', '2026-10-05T00:00:00Z', [eintrag('2026-10-06', 'neu')])], JETZT)
assert.ok(!fx.finde(ohne, '.kn-az-zeilen').classList.contains('kn-az-mit-jahr'))
}))