Eine Seite /verlauf, die den Aktivitaetsverlauf fortlaufend ueber alle Projekte zeigt - ohne Wochengrenze, nach Kalendertagen gegliedert, mit Nachladen aelterer Eintraege. Erste Seite serverseitig, alles Weitere ueber /api/activity. Geblaettert wird per Cursor ueber (createdAt, id), nicht per skip. Der Verlauf schreibt mehrere Eintraege im selben Augenblick - beim Uebernehmen von Issues entstehen zehn Zeilen mit identischem Zeitstempel. Ein Offset ueber eine solche Grenze hinweg ueberspringt Zeilen oder zeigt sie zweimal, und zwar lautlos. Der zweite Zweig der Bedingung (gleicher Zeitpunkt, kleinere ID) faengt genau das ab. Geprueft gegen 144 Eintraege mit absichtlichen Bloecken gleicher Zeitstempel: 15 Seiten, 144 Zeilen, keine Dublette, streng absteigend. Die Bedingung selbst steht als pruefbare Funktion in lib/timeline, samt Deckelung der Seitengroesse und Abweisen unbrauchbarer Cursor - eine ID aus der Abfragezeichenkette geht nicht ungeprueft in die Abfrage. Damit Rueckblick und Zeitleiste wirklich gleich aussehen, sind Zeile und Tagesgruppen nach components/ActivityRow gewandert. Bewusst ohne "use client" und ohne Hooks, damit beides sowohl aus der Server-Komponente (Rueckblick) als auch aus der Client-Komponente (Zeitleiste) heraus benutzbar ist. Die Buendelung im Browser ist unbedenklich, weil tagesschluessel eine feste Zeitzone benutzt - Server und Browser kommen auf denselben Kalendertag. Erreichbar ueber die Schnellsuche und vom Wochenrueckblick aus. Geprueft: 9 neue Unit-Tests zur Cursor-Logik, ein E2E-Test, der die ganze Liste in Dreierseiten durchblaettert und gegen einen einzelnen grossen Aufruf haelt, ein E2E-Test fuer die Seite, axe-core auf 375px und 1280px je vor und nach dem Nachladen ohne Beanstandung. Refs #3
This commit is contained in:
@@ -763,6 +763,60 @@ test("Projektvorlagen: aus Projekt sichern und anwenden", async ({ request, play
|
||||
expect((await request.delete(`/api/templates/${vorlage.id}`)).ok()).toBeTruthy();
|
||||
});
|
||||
|
||||
test("Verlauf blaettert per Cursor, ohne Zeilen zu verlieren", async ({ request, playwright }) => {
|
||||
await login(request, ADMIN);
|
||||
|
||||
// Die bisherigen Tests haben reichlich Verlauf erzeugt. Erst alles auf
|
||||
// einmal holen, dann in kleinen Seiten – beides muss dieselbe Menge sein.
|
||||
const alles = await (await request.get("/api/activity?limit=100")).json();
|
||||
expect(alles.activities.length).toBeGreaterThan(5);
|
||||
|
||||
const ids: string[] = [];
|
||||
let cursor: { vor: string; id: string } | null = null;
|
||||
for (let seite = 0; seite < 60; seite += 1) {
|
||||
const query = cursor
|
||||
? `?limit=3&vor=${encodeURIComponent(cursor.vor)}&id=${encodeURIComponent(cursor.id)}`
|
||||
: "?limit=3";
|
||||
const antwort = await request.get(`/api/activity${query}`);
|
||||
expect(antwort.ok()).toBeTruthy();
|
||||
const seiteBody = await antwort.json();
|
||||
ids.push(...seiteBody.activities.map((eintrag: { id: string }) => eintrag.id));
|
||||
if (!seiteBody.weitere) break;
|
||||
cursor = seiteBody.naechster;
|
||||
expect(cursor).not.toBeNull();
|
||||
}
|
||||
|
||||
// Der eigentliche Punkt: keine Dubletten und keine Luecken, auch wenn
|
||||
// mehrere Eintraege denselben Zeitstempel tragen.
|
||||
expect(new Set(ids).size).toBe(ids.length);
|
||||
expect(ids.length).toBe(alles.activities.length);
|
||||
|
||||
// Absteigend sortiert.
|
||||
const zeiten: number[] = alles.activities.map((eintrag: { createdAt: string }) =>
|
||||
Date.parse(eintrag.createdAt),
|
||||
);
|
||||
for (let i = 1; i < zeiten.length; i += 1) {
|
||||
expect(zeiten[i]).toBeLessThanOrEqual(zeiten[i - 1]);
|
||||
}
|
||||
|
||||
// Unbrauchbare Cursor fangen von vorn an, statt zu scheitern.
|
||||
const kaputt = await request.get("/api/activity?vor=keindatum&id=x");
|
||||
expect(kaputt.ok()).toBeTruthy();
|
||||
expect((await kaputt.json()).activities.length).toBeGreaterThan(0);
|
||||
|
||||
// Und ein fremdes Konto sieht ausschliesslich seinen eigenen Verlauf.
|
||||
const fremd = await playwright.request.newContext({
|
||||
baseURL: test.info().project.use.baseURL,
|
||||
});
|
||||
await login(fremd, ZWEITER);
|
||||
const fremderVerlauf = await (await fremd.get("/api/activity")).json();
|
||||
const eigeneIds = new Set(ids);
|
||||
for (const eintrag of fremderVerlauf.activities) {
|
||||
expect(eigeneIds.has(eintrag.id)).toBe(false);
|
||||
}
|
||||
await fremd.dispose();
|
||||
});
|
||||
|
||||
test("Der Bildoptimierer ist zu", async ({ playwright }) => {
|
||||
// /_next/image reicht jedes Bild durch sharp und damit durch libvips, fuer
|
||||
// das es regelmaessig Schwachstellen gibt. Nebula benutzt next/image
|
||||
|
||||
@@ -249,6 +249,26 @@ test("Wochenrueckblick zeigt, was diese Woche passiert ist", async ({ page }) =>
|
||||
await expect(page).toHaveURL(/\/rueckblick/);
|
||||
});
|
||||
|
||||
test("Globale Zeitleiste zeigt den Verlauf ueber alle Projekte", async ({ page }) => {
|
||||
// Vom Wochenrueckblick aus verlinkt.
|
||||
await page.goto("/rueckblick");
|
||||
await page.getByRole("link", { name: /Ganzer Verlauf/ }).click();
|
||||
await expect(page).toHaveURL(/\/verlauf$/);
|
||||
await expect(page.getByRole("heading", { name: "Verlauf", level: 1 })).toBeVisible();
|
||||
|
||||
// Eintraege mit Projektbezug – ohne den ist eine uebergreifende Liste
|
||||
// nicht zu lesen.
|
||||
await expect(page.getByRole("link", { name: "Oberflaechentest" }).first()).toBeVisible();
|
||||
await expect(page.getByText(/Vorgänge geladen|Vorgang geladen/)).toBeVisible();
|
||||
|
||||
// Auch ueber die Schnellsuche erreichbar.
|
||||
await page.goto("/");
|
||||
await page.keyboard.press("Control+k");
|
||||
await page.getByPlaceholder(/Projekt, Notiz oder Bereich/).fill("Verlauf");
|
||||
await page.keyboard.press("Enter");
|
||||
await expect(page).toHaveURL(/\/verlauf/);
|
||||
});
|
||||
|
||||
test("Schnellsuche findet Projekt und Notiz", async ({ page }) => {
|
||||
await page.keyboard.press("Control+k");
|
||||
await expect(page.getByRole("dialog", { name: "Schnellsuche" })).toBeVisible();
|
||||
|
||||
Reference in New Issue
Block a user