Ein Feld je Tag auf der Projektseite, Wochen als Spalten, mit Farbskala und Datum im Tooltip. Woher die Zahlen kommen, unterscheidet sich je Anbieter. GitHub fuehrt die Statistik selbst und liefert 52 Wochen mit Tageswerten in einem einzigen Aufruf. GitLab und Gitea kennen nichts Vergleichbares; dort werden Commits seitenweise geholt, gedeckelt auf fuenf Seiten - genug fuer die allermeisten Repositories und wenig genug, um kein fremdes Kontingent zu sprengen. Gezeichnet wird nur der Zeitraum, fuer den wirklich Daten vorliegen. Reichen die Seiten nur ein paar Monate zurueck, faengt die Heatmap dort an und sagt es dazu. Ein leeres Feld wuerde sonst behaupten, an dem Tag sei nicht gearbeitet worden, obwohl darueber nichts bekannt ist - genau diesen Fehler hatte die Verlaufslinie schon einmal. Geholt wird hoechstens einmal am Tag und getrennt vom uebrigen Abgleich: an einem Jahresueberblick aendert sich von Stunde zu Stunde nichts Sichtbares, der Abgleich laeuft aber im Zweifel stuendlich. Der Kalender benutzt getJson statt getJsonSafe. Waere ein Rate-Limit als "keine Daten" durchgegangen, haette der gesetzte Zeitstempel die Heatmap einen ganzen Tag lang unterdrueckt. Jetzt bleibt er bei einem Fehlschlag ungesetzt, und der naechste Abgleich versucht es erneut - das deckt auch den Fall ab, dass GitHub die Statistik gerade erst berechnet und mit 202 antwortet. Nachgewiesen an vercel/swr: 49 Commits an 33 Tagen ueber ein volles Jahr aus der Statistik, und mit kurzer Historie 90 statt 365 Felder samt Hinweis auf den Deckel. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
123 lines
4.2 KiB
TypeScript
123 lines
4.2 KiB
TypeScript
import { describe, expect, it, vi } from "vitest";
|
||
import {
|
||
CALENDAR_MAX_PAGES,
|
||
CALENDAR_PAGE_SIZE,
|
||
calendarByPaging,
|
||
calendarSince,
|
||
calendarTotal,
|
||
dayKey,
|
||
} from "./calendar";
|
||
|
||
/**
|
||
* Der Kalender darf ueber den Zeitraum nicht luegen. Reichen die geholten
|
||
* Seiten nur ein paar Monate zurueck, faengt die Heatmap dort an – sie zeigt
|
||
* dann keine leeren Felder fuer Zeiten, ueber die schlicht nichts bekannt ist.
|
||
* Genau diesen Fehler hatte die Verlaufslinie schon einmal.
|
||
*/
|
||
|
||
const JETZT = new Date("2026-07-26T12:00:00.000Z");
|
||
const TAG = 86_400_000;
|
||
|
||
/** Erzeugt n Commit-Zeitpunkte, beginnend vor `abTagen` Tagen, einer je Tag. */
|
||
function commits(anzahl: number, abTagen: number): string[] {
|
||
return Array.from({ length: anzahl }, (_, index) =>
|
||
new Date(JETZT.getTime() - (abTagen + index) * TAG).toISOString(),
|
||
);
|
||
}
|
||
|
||
describe("dayKey", () => {
|
||
it("liefert das Datum ohne Uhrzeit", () => {
|
||
expect(dayKey(new Date("2026-07-26T23:59:00.000Z"))).toBe("2026-07-26");
|
||
});
|
||
});
|
||
|
||
describe("calendarSince", () => {
|
||
it("liegt ein Jahr zurueck", () => {
|
||
const seit = calendarSince(JETZT);
|
||
expect(Math.round((JETZT.getTime() - seit.getTime()) / TAG)).toBe(365);
|
||
});
|
||
});
|
||
|
||
describe("calendarByPaging", () => {
|
||
it("liefert null, wenn es keine Commits gibt", async () => {
|
||
expect(await calendarByPaging(async () => [], JETZT)).toBeNull();
|
||
});
|
||
|
||
it("zaehlt mehrere Commits desselben Tages zusammen", async () => {
|
||
const heute = JETZT.toISOString();
|
||
const kalender = await calendarByPaging(async () => [heute, heute, heute], JETZT);
|
||
expect(kalender?.days["2026-07-26"]).toBe(3);
|
||
expect(calendarTotal(kalender!)).toBe(3);
|
||
});
|
||
|
||
it("beginnt beim aeltesten bekannten Commit, nicht vor einem Jahr", async () => {
|
||
// Nur eine halbe Seite, zehn Tage zurueck: alles davor ist unbekannt.
|
||
const kalender = await calendarByPaging(async () => commits(10, 0), JETZT);
|
||
expect(kalender?.from).toBe("2026-07-17");
|
||
expect(kalender?.to).toBe("2026-07-26");
|
||
expect(kalender?.truncated).toBe(false);
|
||
});
|
||
|
||
it("hoert auf, sobald eine Seite nicht mehr voll ist", async () => {
|
||
const ladeSeite = vi.fn(async (seite: number) =>
|
||
seite === 1 ? commits(CALENDAR_PAGE_SIZE, 0) : commits(5, CALENDAR_PAGE_SIZE),
|
||
);
|
||
await calendarByPaging(ladeSeite, JETZT);
|
||
expect(ladeSeite).toHaveBeenCalledTimes(2);
|
||
});
|
||
|
||
it("deckelt die Zahl der Seiten und sagt es", async () => {
|
||
// Immer volle Seiten innerhalb des Zeitraums: ohne Deckel liefe das ewig.
|
||
const ladeSeite = vi.fn(async () =>
|
||
Array.from({ length: CALENDAR_PAGE_SIZE }, () =>
|
||
new Date(JETZT.getTime() - 3 * TAG).toISOString(),
|
||
),
|
||
);
|
||
const kalender = await calendarByPaging(ladeSeite, JETZT);
|
||
expect(ladeSeite).toHaveBeenCalledTimes(CALENDAR_MAX_PAGES);
|
||
expect(kalender?.truncated).toBe(true);
|
||
});
|
||
|
||
it("bricht ab, wenn eine ganze Seite aelter ist als der Zeitraum", async () => {
|
||
const ladeSeite = vi.fn(async (seite: number) =>
|
||
seite === 1
|
||
? commits(CALENDAR_PAGE_SIZE, 0)
|
||
: Array.from({ length: CALENDAR_PAGE_SIZE }, () =>
|
||
new Date(JETZT.getTime() - 400 * TAG).toISOString(),
|
||
),
|
||
);
|
||
await calendarByPaging(ladeSeite, JETZT);
|
||
expect(ladeSeite).toHaveBeenCalledTimes(2);
|
||
});
|
||
|
||
it("uebergeht Commits mit Datum in der Zukunft", async () => {
|
||
// Falsch gestellte Uhren gibt es; sie duerfen den Zeitraum nicht verlaengern.
|
||
const kalender = await calendarByPaging(
|
||
async () => [
|
||
new Date(JETZT.getTime() + 5 * TAG).toISOString(),
|
||
JETZT.toISOString(),
|
||
],
|
||
JETZT,
|
||
);
|
||
expect(kalender?.to).toBe("2026-07-26");
|
||
expect(calendarTotal(kalender!)).toBe(1);
|
||
});
|
||
|
||
it("uebergeht Unbrauchbares, statt daran zu scheitern", async () => {
|
||
const kalender = await calendarByPaging(
|
||
async () => [null, "gestern", "", JETZT.toISOString()],
|
||
JETZT,
|
||
);
|
||
expect(calendarTotal(kalender!)).toBe(1);
|
||
});
|
||
|
||
it("laesst Tage ohne Commits weg", async () => {
|
||
// Ein Jahr als Liste waeren 365 Eintraege, die meisten davon Nullen.
|
||
const kalender = await calendarByPaging(
|
||
async () => [JETZT.toISOString(), new Date(JETZT.getTime() - 30 * TAG).toISOString()],
|
||
JETZT,
|
||
);
|
||
expect(Object.keys(kalender!.days)).toHaveLength(2);
|
||
});
|
||
});
|