Ampel / ampel (push) Failing after 25s
Der Zugewinn gegenueber einer reinen Liste: Ein stiller Tag und eine durchgearbeitete
Nacht sehen in einer Liste gleich aus — man muss scrollen, um es zu merken. Die
Dichteleiste macht das Muster sichtbar, bevor man den ersten Eintrag liest.
48 Stufen ueber den gezeigten Zeitraum, Achsenbeschriftung an beiden Enden, und beim
Filtern zeigt sie nur die Treffer (dann in kraeftigerem Ton).
Bewusst eigenes SVG-freies Markup statt einer Diagramm-Bibliothek: Es sind Rechtecke
ueber einer Zeitachse. Recharts dafuer zu laden waere 95 kB fuer etwas, das in 30
Zeilen passt — genau der Reflex, der in P0 das Startbuendel aufgeblaeht hatte.
Eine Kleinigkeit mit Absicht: Ein einzelner Eintrag bekommt 12 % Mindesthoehe. Ohne
sie saehe ein ruhiger Tag aus wie gar keine Aktivitaet — genau die Verwechslung, die
die Leiste verhindern soll. Dafuer gibt es einen eigenen Test.
Verifiziert gegen die echte Box:
150 Eintraege vom 02.08. bis 28.08., 25 von 48 Stufen belegt, Hoehen gestaffelt
Filter "update" -> 30 von 150, Leiste zeigt nur noch die Treffer und wird
hervorgehoben, Adresse /chronik?q=update
59 Tests gruen (5 neue) · ESLint 0 Fehler · Einstieg 133 400 B gzip / Budget 140 000.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
60 lines
2.5 KiB
TypeScript
60 lines
2.5 KiB
TypeScript
import { describe, expect, it } from "vitest"
|
|
import { render } from "@testing-library/react"
|
|
import { Dichteleiste } from "./Dichteleiste"
|
|
import type { ChronikItem } from "@/lib/api"
|
|
|
|
function eintrag(ts: number): ChronikItem {
|
|
return { id: ts, ts, subject: "x", text: "y", source: "test", priority: "normal" }
|
|
}
|
|
|
|
/** Die Balken der Leiste (ohne die Achsen-Beschriftung darunter). */
|
|
function balken(container: HTMLElement): HTMLElement[] {
|
|
return [...container.querySelectorAll('[role="img"] > div')] as HTMLElement[]
|
|
}
|
|
|
|
describe("Dichteleiste", () => {
|
|
it("zeigt nichts, wenn es nichts zu zeigen gibt", () => {
|
|
const { container } = render(<Dichteleiste items={[]} />)
|
|
expect(container).toBeEmptyDOMElement()
|
|
})
|
|
|
|
it("verteilt die Einträge über die volle Breite", () => {
|
|
const basis = 1_800_000_000
|
|
const items = [eintrag(basis), eintrag(basis + 43_200), eintrag(basis + 86_400)]
|
|
const { container } = render(<Dichteleiste items={items} />)
|
|
const b = balken(container)
|
|
expect(b).toHaveLength(48)
|
|
// Erster, mittlerer und letzter Eimer sind belegt — dazwischen ist es leer.
|
|
expect(b[0].style.height).not.toBe("2px")
|
|
expect(b[47].style.height).not.toBe("2px")
|
|
expect(b[5].style.height).toBe("2px")
|
|
})
|
|
|
|
// Ohne Mindesthöhe sähe ein ruhiger Tag aus wie gar keine Aktivität — genau die
|
|
// Verwechslung, die die Leiste verhindern soll.
|
|
it("macht auch einen einzelnen Eintrag sichtbar", () => {
|
|
const { container } = render(<Dichteleiste items={[eintrag(1_800_000_000)]} />)
|
|
const belegt = balken(container).filter((d) => d.style.height !== "2px")
|
|
expect(belegt).toHaveLength(1)
|
|
expect(parseFloat(belegt[0].style.height)).toBeGreaterThanOrEqual(12)
|
|
})
|
|
|
|
it("beschriftet sich für Screenreader mit Anzahl und Zeitraum", () => {
|
|
const basis = 1_800_000_000
|
|
const { container } = render(<Dichteleiste items={[eintrag(basis), eintrag(basis + 86_400)]} />)
|
|
const label = container.querySelector('[role="img"]')!.getAttribute("aria-label")!
|
|
expect(label).toMatch(/2 Einträge/)
|
|
expect(label).toMatch(/zwischen .* und /)
|
|
})
|
|
|
|
it("hebt die Leiste hervor, solange gefiltert wird", () => {
|
|
const items = [eintrag(1_800_000_000)]
|
|
const normal = render(<Dichteleiste items={items} />).container
|
|
const gefiltert = render(<Dichteleiste items={items} aktiv />).container
|
|
const klasse = (c: HTMLElement) =>
|
|
balken(c).find((d) => d.style.height !== "2px")!.className
|
|
expect(klasse(normal)).toMatch(/primary\/40/)
|
|
expect(klasse(gefiltert)).toMatch(/primary\/70/)
|
|
})
|
|
})
|