**Der schwarze Bildschirm ist gefunden.** `GET /retention-policies` liefert ein
Objekt `{ policies, predefined }` — als einziger von zehn geprueften
Listenendpunkten. Die Oberflaeche behandelte es als Array; `map` gibt es auf
einem Objekt nicht, React haengte den ganzen Baum aus, und uebrig blieb eine
leere Seite. Im dunklen Thema schwarz.
Der Regressionstest fuettert die **echte** Antwortform. Ein Test mit einem Array
haette den Fehler nie gefunden — und genau das war passiert. Durch Mutation als
fangend bestaetigt ("repositories.map is not a function").
Nebenbei ergab sich ein Modul: Der Endpunkt liefert **sechs mitgelieferte
Vorlagen** mit. Sie erscheinen jetzt als Kacheln, ein Klick legt daraus eine
eigene Regel an.
**Aussehen.** Kraeftigerer Akzent (Indigo statt gedecktem Blau), waermere
Neutraltoene, groessere Radien, Schatten statt Rahmen, wo Tiefe gemeint ist:
Ein Rahmen trennt, ein Schatten hebt — Karten sollen heben, Tabellenzeilen
trennen. Kompaktere Statusabzeichen und Tabellenkoepfe.
**Texte gekuerzt.** 33 Erlaeuterungen von Absaetzen auf einen Satz. Der Inhalt
bleibt richtig; er steht nur nicht mehr zwischen den Bedienelementen. Was
ausfuehrlich erklaert werden muss, gehoert in docs/.
**Schnellzugriffe auf der Uebersicht.** Acht Kacheln, die die Handlung nennen
statt des Bereichs ("Auftrag anlegen" statt "Sicherungsauftraege"). Gezeigt wird
nur, was die Rolle darf — eine Kachel, die in eine Fehlermeldung fuehrt, ist
schlimmer als keine. Als Schaltflaechen umgesetzt, nicht als div mit
Klick-Handler: Der Unterschied ist Tastaturbedienbarkeit.
84 Tests gruen, tsc sauber, eslint ohne Warnung.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
103 lines
3.1 KiB
TypeScript
103 lines
3.1 KiB
TypeScript
/**
|
|
* Prüft die beiden Grenzen der Sitzung.
|
|
*
|
|
* Beide sind eine Zusage an den Betreiber, und beide lassen sich mit einer
|
|
* falschen Zeile still aushebeln: Wer beim Vermerken einer Interaktion die
|
|
* harte Obergrenze mitverschiebt, macht aus „30 Minuten" ein „unbegrenzt,
|
|
* solange die Maus wackelt".
|
|
*/
|
|
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
import {
|
|
SESSION_IDLE_MS,
|
|
SESSION_MAXIMUM_MS,
|
|
clearStoredSession,
|
|
readStoredSession,
|
|
remainingSessionMs,
|
|
touchStoredSession,
|
|
writeStoredSession,
|
|
} from './sessionStore';
|
|
|
|
const tokenPair = {
|
|
access_token: 'zugriff',
|
|
refresh_token: 'erneuerung', // secretscan:erlaubt — Testwert, kein Geheimnis
|
|
access_expires_at: '',
|
|
refresh_expires_at: '',
|
|
};
|
|
|
|
describe('Sitzungsablage', () => {
|
|
beforeEach(() => {
|
|
vi.useFakeTimers();
|
|
vi.setSystemTime(new Date('2026-08-18T10:00:00Z'));
|
|
clearStoredSession();
|
|
});
|
|
|
|
afterEach(() => {
|
|
vi.useRealTimers();
|
|
});
|
|
|
|
it('überlebt ein Neuladen innerhalb der Frist', () => {
|
|
writeStoredSession(tokenPair);
|
|
|
|
// Ein Neuladen bedeutet: neuer Arbeitsspeicher, gleiche Ablage.
|
|
const resumed = readStoredSession();
|
|
|
|
expect(resumed).not.toBeNull();
|
|
expect(resumed?.accessToken).toBe('zugriff');
|
|
});
|
|
|
|
it('endet nach der harten Obergrenze, auch bei ständiger Interaktion', () => {
|
|
writeStoredSession(tokenPair);
|
|
|
|
// Alle fünf Minuten eine Interaktion — sieben Mal, also 35 Minuten.
|
|
for (let step = 0; step < 7; step += 1) {
|
|
vi.advanceTimersByTime(5 * 60 * 1000);
|
|
touchStoredSession();
|
|
}
|
|
|
|
// Die Untätigkeitsgrenze wäre nie erreicht worden. Die Obergrenze schon.
|
|
expect(readStoredSession()).toBeNull();
|
|
});
|
|
|
|
it('endet bei Untätigkeit vor der Obergrenze', () => {
|
|
writeStoredSession(tokenPair);
|
|
|
|
vi.advanceTimersByTime(SESSION_IDLE_MS + 1000);
|
|
|
|
expect(readStoredSession()).toBeNull();
|
|
});
|
|
|
|
it('verschiebt die Obergrenze durch eine Interaktion nicht', () => {
|
|
const created = writeStoredSession(tokenPair);
|
|
|
|
vi.advanceTimersByTime(60 * 1000);
|
|
const touched = touchStoredSession();
|
|
|
|
expect(touched?.absoluteExpiryMs).toBe(created.absoluteExpiryMs);
|
|
expect(touched?.lastActivityMs).toBeGreaterThan(created.lastActivityMs);
|
|
});
|
|
|
|
it('nennt die frühere der beiden Grenzen als Restzeit', () => {
|
|
const created = writeStoredSession(tokenPair);
|
|
|
|
// Direkt nach der Anmeldung fallen beide Grenzen zusammen.
|
|
expect(remainingSessionMs(created)).toBe(SESSION_MAXIMUM_MS);
|
|
|
|
vi.advanceTimersByTime(20 * 60 * 1000);
|
|
|
|
const stillValid = readStoredSession();
|
|
|
|
expect(stillValid).not.toBeNull();
|
|
// Zehn Minuten bis zur Obergrenze, dreißig bis zur Untätigkeitsgrenze —
|
|
// maßgeblich sind die zehn.
|
|
expect(remainingSessionMs(stillValid!)).toBe(10 * 60 * 1000);
|
|
});
|
|
|
|
it('verwirft einen unlesbaren Eintrag, statt ihn zu raten', () => {
|
|
window.sessionStorage.setItem('syncova.session', '{kein gültiges json');
|
|
|
|
expect(readStoredSession()).toBeNull();
|
|
expect(window.sessionStorage.getItem('syncova.session')).toBeNull();
|
|
});
|
|
});
|