Compare commits
3 Commits
5919a55cbf
..
live
| Author | SHA1 | Date | |
|---|---|---|---|
| 645c5e5887 | |||
| 2dd11b439d | |||
| 59b8cd43fc |
@@ -484,6 +484,8 @@ Gerettet aus `.continue-here.md`. Relevant fuer die noch offenen Live-Tests.
|
|||||||
| 260929-if2 | **Erinnerungen-Widget (Reminder).** Modell `Reminder` + RLS, API /reminders (anlegen/listen/bearbeiten/loeschen/erledigt/snooze, 409/404-Regeln), E-Mail-Scheduler alle 30 s mit Claim-once + max. 3 Versuche, globaler ReminderNotifier (Browser-Notification, Desktop via Tauri-Notification mit Laufzeit-Capability nur fuer die Server-Origin, Pattern escaped + vorab geprueft). Verifier human_needed (Windows-Toast offen); Browser dunkel bestanden inkl. echter Mail ueber MailHog. api 1570, web 1069, cargo 57. Nebenbei: eigene Module ohne Kopfzeile (cd1f8f6), Update-Klick prueft frisch (41d00a3). | 2026-09-29 | 325c5dd,709b41a,6879c75 | [260929-if2-reminder-widget-mit-benachrichtigung](./quick/260929-if2-reminder-widget-mit-benachrichtigung/) |
|
| 260929-if2 | **Erinnerungen-Widget (Reminder).** Modell `Reminder` + RLS, API /reminders (anlegen/listen/bearbeiten/loeschen/erledigt/snooze, 409/404-Regeln), E-Mail-Scheduler alle 30 s mit Claim-once + max. 3 Versuche, globaler ReminderNotifier (Browser-Notification, Desktop via Tauri-Notification mit Laufzeit-Capability nur fuer die Server-Origin, Pattern escaped + vorab geprueft). Verifier human_needed (Windows-Toast offen); Browser dunkel bestanden inkl. echter Mail ueber MailHog. api 1570, web 1069, cargo 57. Nebenbei: eigene Module ohne Kopfzeile (cd1f8f6), Update-Klick prueft frisch (41d00a3). | 2026-09-29 | 325c5dd,709b41a,6879c75 | [260929-if2-reminder-widget-mit-benachrichtigung](./quick/260929-if2-reminder-widget-mit-benachrichtigung/) |
|
||||||
| 260929-lh3 | **Favoriten: eigene Symbol-Adresse wirkt.** Neue iconUrl ersetzt Upload + bumpt iconVersion; iconUrl wird auch gespeichert, wenn nur der Browser sie laden kann (kein 422 mehr, nur Formpruefung); Kachel: Proxy -> iconUrl direkt -> origin/favicon -> Buchstabe; Discovery liest <link rel=icon> auch aus Nicht-2xx-Seiten (docuvita 400). | 2026-09-29 | 7188c5b,b15c746,0e72ad4 | [260929-lh3-favoriten-eigenes-symbol-wirkt-nicht](./quick/260929-lh3-favoriten-eigenes-symbol-wirkt-nicht/) |
|
| 260929-lh3 | **Favoriten: eigene Symbol-Adresse wirkt.** Neue iconUrl ersetzt Upload + bumpt iconVersion; iconUrl wird auch gespeichert, wenn nur der Browser sie laden kann (kein 422 mehr, nur Formpruefung); Kachel: Proxy -> iconUrl direkt -> origin/favicon -> Buchstabe; Discovery liest <link rel=icon> auch aus Nicht-2xx-Seiten (docuvita 400). | 2026-09-29 | 7188c5b,b15c746,0e72ad4 | [260929-lh3-favoriten-eigenes-symbol-wirkt-nicht](./quick/260929-lh3-favoriten-eigenes-symbol-wirkt-nicht/) |
|
||||||
| 261001-cxo | Desktop-Client: Links mit target=_blank (Favoriten) oeffnen jetzt im System-Browser (DesktopExternalLinks -> window.open) | 2026-10-01 | 61a971c | [261001-cxo](./quick/261001-cxo-desktop-client-links-mit-target-blank-oe/) |
|
| 261001-cxo | Desktop-Client: Links mit target=_blank (Favoriten) oeffnen jetzt im System-Browser (DesktopExternalLinks -> window.open) | 2026-10-01 | 61a971c | [261001-cxo](./quick/261001-cxo-desktop-client-links-mit-target-blank-oe/) |
|
||||||
|
| 261001-g68 | Erinnerung: Cursor sprang beim Schreiben der Beschreibung in den Titel (Fokus-Effekt hing an inline onClose, Kachel zeichnet alle 10 s neu) – Fokus nur beim Oeffnen | 2026-10-01 | siehe git log | [261001-g68](./quick/261001-g68-erinnerung-cursor-springt-aus-beschreibu/) |
|
||||||
|
| 261001-hbi | Favoriten: Logo fuer per JavaScript gesetzte Symbole (hosteurope.de) – Rueckfall auf DuckDuckGo-Symboldienst beim Ausliefern, nur oeffentliche Seiten | 2026-10-01 | siehe git log | [261001-hbi](./quick/261001-hbi-favoriten-logo-fuer-per-javascript-geset/) |
|
||||||
|
|
||||||
## Deferred Items
|
## Deferred Items
|
||||||
|
|
||||||
|
|||||||
+14
@@ -0,0 +1,14 @@
|
|||||||
|
---
|
||||||
|
quick_id: 261001-g68
|
||||||
|
description: "Erinnerung: Cursor springt aus Beschreibung in Titel"
|
||||||
|
date: 2026-10-01
|
||||||
|
---
|
||||||
|
|
||||||
|
# Erinnerung: Cursor springt aus Beschreibung in Titel
|
||||||
|
|
||||||
|
**Befund:** `ReminderFormModal` setzte den Fokus auf den Titel im selben Effekt wie den Escape-Listener, Abhaengigkeit `[onClose]`. `onClose` ist in der Kachel eine Inline-Funktion; die Kachel zeichnet alle 10 s neu (NOW_TICK_MS) und bei jedem Neuladen → Effekt laeuft erneut → Cursor springt in den Titel (User: beim Schreiben, und bei Loeschen-Taste in leerer Beschreibung).
|
||||||
|
|
||||||
|
## Task 1
|
||||||
|
- Fokus-Effekt nur beim Oeffnen (`[]`), Escape-Listener ueber `onCloseRef`.
|
||||||
|
- Test im Widget: Formular oeffnen, Beschreibung fokussieren, 30 s Takt → Fokus bleibt.
|
||||||
|
- CHANGELOG „Unveröffentlicht → Behoben“.
|
||||||
+10
@@ -0,0 +1,10 @@
|
|||||||
|
---
|
||||||
|
quick_id: 261001-g68
|
||||||
|
status: complete
|
||||||
|
date: 2026-10-01
|
||||||
|
---
|
||||||
|
|
||||||
|
# Summary
|
||||||
|
- `reminder-form-modal.tsx`: Fokus auf Titel nur einmal beim Oeffnen; Escape ueber Ref statt `[onClose]`-Abhaengigkeit.
|
||||||
|
- Neuer Test in `reminder-widget.test.tsx` – schlaegt ohne Fix fehl, mit Fix gruen; 28/28 Erinnerungs-Tests, tsc, biome sauber.
|
||||||
|
- Andere Dialoge mit `[onClose]`-Fokus (Widget-Katalog, Bilderrahmen-Lightbox) fokussieren nur den Dialog ohne Eingabefelder – nicht betroffen.
|
||||||
+14
@@ -0,0 +1,14 @@
|
|||||||
|
---
|
||||||
|
quick_id: 261001-hbi
|
||||||
|
description: "Favoriten: Logo fuer per JavaScript gesetzte Symbole"
|
||||||
|
date: 2026-10-01
|
||||||
|
---
|
||||||
|
|
||||||
|
# Favoriten: Logo fuer per JavaScript gesetzte Symbole
|
||||||
|
|
||||||
|
**Befund:** https://www.hosteurope.de/ liefert im HTML nur `<link rel="icon" href="data:;base64,=">`; das echte Symbol (img1.wsimg.com/.../HostEurope.png) setzt erst JavaScript. `/favicon.ico`, `/apple-touch-icon.png`, `/favicon.svg` antworten 200 mit text/html. Die serverseitige Suche faellt auf `/favicon.ico` zurueck, der Abruf scheitert (kein Bild) → Buchstabe.
|
||||||
|
|
||||||
|
## Task 1
|
||||||
|
- `IconDiscoveryService.fetchPublicServiceIconBytes(pageUrl)`: DuckDuckGo-Symboldienst (`icons.duckduckgo.com/ip3/<host>.ico`), NUR wenn die Seite oeffentlich ist (isPublicHttpUrl) — interne Hostnamen verlassen das Haus nicht; 404 fuer Unbekanntes → wirft → Buchstabe bleibt.
|
||||||
|
- `FavoritesService.getIconBytes`: scheitert das gespeicherte Symbol, einmal den Dienst fragen, sonst 502 wie bisher. Repariert auch bestehende Favoriten ohne Neuanlage.
|
||||||
|
- Tests in beiden Specs; CHANGELOG.
|
||||||
+10
@@ -0,0 +1,10 @@
|
|||||||
|
---
|
||||||
|
quick_id: 261001-hbi
|
||||||
|
status: complete
|
||||||
|
date: 2026-10-01
|
||||||
|
---
|
||||||
|
|
||||||
|
# Summary
|
||||||
|
- Rueckfall auf den oeffentlichen Symbol-Dienst beim Ausliefern (`getIconBytes`), nur fuer oeffentliche Seiten.
|
||||||
|
- 5 neue Tests (Dienst-URL, interne Seite fragt nicht, 404 wirft, Service nutzt Rueckfall / nicht bei Erfolg); 111/111 Favoriten-Tests, tsc, biome-Stand unveraendert.
|
||||||
|
- Lokal im Browser nachgewiesen: Favorit https://www.hosteurope.de/ zeigt das gruene H-Logo (32x32 ueber /api-proxy/favorites/<id>/icon).
|
||||||
@@ -4,9 +4,13 @@ Diese Liste beschreibt in einfachen Worten, was sich von Version zu Version an T
|
|||||||
|
|
||||||
## Unveröffentlicht
|
## Unveröffentlicht
|
||||||
|
|
||||||
|
## 1.9.1 – 2026-10-01
|
||||||
|
|
||||||
### Behoben
|
### Behoben
|
||||||
|
|
||||||
- Desktop-App: Favoriten und andere Links, die sich in einem neuen Fenster öffnen (etwa „In neuem Tab öffnen“ oder Quellen im Ausschreibungs-Radar), öffnen sich jetzt in Ihrem normalen Browser. Bisher passierte beim Klick in der Desktop-App nichts.
|
- Desktop-App: Favoriten und andere Links, die sich in einem neuen Fenster öffnen (etwa „In neuem Tab öffnen“ oder Quellen im Ausschreibungs-Radar), öffnen sich jetzt in Ihrem normalen Browser. Bisher passierte beim Klick in der Desktop-App nichts.
|
||||||
|
- Erinnerungen: Beim Schreiben der Beschreibung springt der Cursor nicht mehr in die Titelzeile zurück. Bisher passierte das alle paar Sekunden, weil sich die Kachel regelmäßig neu aufbaut.
|
||||||
|
- Favoriten: Auch Seiten, die ihr Logo erst beim Laden im Browser setzen (etwa Host Europe), zeigen jetzt ihr Logo statt nur des Anfangsbuchstabens. Findet Tessera auf der Seite selbst kein Logo, fragt es bei öffentlichen Adressen einen Logo-Dienst; interne Adressen werden dabei nie weitergegeben. Das gilt auch für bereits angelegte Favoriten.
|
||||||
|
|
||||||
## 1.9.0 – 2026-09-30
|
## 1.9.0 – 2026-09-30
|
||||||
|
|
||||||
|
|||||||
@@ -220,7 +220,11 @@ function expectBoundCall(
|
|||||||
}
|
}
|
||||||
|
|
||||||
function makeIconDiscovery(
|
function makeIconDiscovery(
|
||||||
overrides: Partial<{ discoverFavoriteIconUrl: any; fetchIconBytes: any }> = {},
|
overrides: Partial<{
|
||||||
|
discoverFavoriteIconUrl: any;
|
||||||
|
fetchIconBytes: any;
|
||||||
|
fetchPublicServiceIconBytes: any;
|
||||||
|
}> = {},
|
||||||
) {
|
) {
|
||||||
return {
|
return {
|
||||||
discoverFavoriteIconUrl:
|
discoverFavoriteIconUrl:
|
||||||
@@ -229,6 +233,11 @@ function makeIconDiscovery(
|
|||||||
fetchIconBytes:
|
fetchIconBytes:
|
||||||
overrides.fetchIconBytes ??
|
overrides.fetchIconBytes ??
|
||||||
vi.fn(async () => ({ contentType: 'image/png', body: Buffer.from('png') })),
|
vi.fn(async () => ({ contentType: 'image/png', body: Buffer.from('png') })),
|
||||||
|
fetchPublicServiceIconBytes:
|
||||||
|
overrides.fetchPublicServiceIconBytes ??
|
||||||
|
vi.fn(async () => {
|
||||||
|
throw new Error('icon service: unknown');
|
||||||
|
}),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -511,6 +520,35 @@ describe('FavoritesService — Bindung an forTenant() (260911-gwh)', () => {
|
|||||||
await expect(service.getIconBytes('t2', 'f1', 'user-a1')).rejects.toThrow(NotFoundException);
|
await expect(service.getIconBytes('t2', 'f1', 'user-a1')).rejects.toThrow(NotFoundException);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('quick-261001-hbi: gespeichertes Symbol scheitert -> Symbol-Dienst mit der Seiten-URL', async () => {
|
||||||
|
const prisma = makeFakePrisma([baseRow]);
|
||||||
|
const iconDiscovery = makeIconDiscovery({
|
||||||
|
fetchIconBytes: vi.fn(async () => {
|
||||||
|
throw new Error('not an image');
|
||||||
|
}),
|
||||||
|
fetchPublicServiceIconBytes: vi.fn(async () => ({
|
||||||
|
contentType: 'image/png',
|
||||||
|
body: Buffer.from('ddg'),
|
||||||
|
})),
|
||||||
|
});
|
||||||
|
const service = new FavoritesService(prisma as any, iconDiscovery as any);
|
||||||
|
|
||||||
|
const result = await service.getIconBytes('t1', 'f1', 'user-a1');
|
||||||
|
|
||||||
|
expect(iconDiscovery.fetchPublicServiceIconBytes).toHaveBeenCalledWith(baseRow.url);
|
||||||
|
expect(result.body).toEqual(Buffer.from('ddg'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('quick-261001-hbi: gespeichertes Symbol klappt -> Symbol-Dienst wird nicht gefragt', async () => {
|
||||||
|
const prisma = makeFakePrisma([baseRow]);
|
||||||
|
const iconDiscovery = makeIconDiscovery();
|
||||||
|
const service = new FavoritesService(prisma as any, iconDiscovery as any);
|
||||||
|
|
||||||
|
await service.getIconBytes('t1', 'f1', 'user-a1');
|
||||||
|
|
||||||
|
expect(iconDiscovery.fetchPublicServiceIconBytes).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
it('fetchIconBytes wirft -> HttpException mit Status 502', async () => {
|
it('fetchIconBytes wirft -> HttpException mit Status 502', async () => {
|
||||||
const prisma = makeFakePrisma([baseRow]);
|
const prisma = makeFakePrisma([baseRow]);
|
||||||
const iconDiscovery = makeIconDiscovery({
|
const iconDiscovery = makeIconDiscovery({
|
||||||
|
|||||||
@@ -497,7 +497,9 @@ export class FavoritesService {
|
|||||||
* Throws NotFoundException (404) if the row doesn't exist, isn't owned
|
* Throws NotFoundException (404) if the row doesn't exist, isn't owned
|
||||||
* by the caller, or has neither an uploaded icon nor a stored iconUrl.
|
* by the caller, or has neither an uploaded icon nor a stored iconUrl.
|
||||||
* Throws a 502 HttpException if the upstream fetch fails (unreachable,
|
* Throws a 502 HttpException if the upstream fetch fails (unreachable,
|
||||||
* timeout, non-image, or SSRF-blocked) -- never returns a placeholder image.
|
* timeout, non-image, or SSRF-blocked) AND the public icon service fallback
|
||||||
|
* (quick-261001-hbi, public pages only) has no icon either -- never returns
|
||||||
|
* a placeholder image.
|
||||||
*/
|
*/
|
||||||
async getIconBytes(
|
async getIconBytes(
|
||||||
tenantId: string,
|
tenantId: string,
|
||||||
@@ -536,7 +538,14 @@ export class FavoritesService {
|
|||||||
try {
|
try {
|
||||||
return await this.iconDiscovery.fetchIconBytes(link.iconUrl);
|
return await this.iconDiscovery.fetchIconBytes(link.iconUrl);
|
||||||
} catch {
|
} catch {
|
||||||
throw new HttpException('Icon fetch failed', HttpStatus.BAD_GATEWAY);
|
// quick-261001-hbi: Seite liefert kein abrufbares Symbol (z. B. per
|
||||||
|
// JavaScript gesetzt) -- einmal beim oeffentlichen Symbol-Dienst fragen,
|
||||||
|
// nur fuer oeffentlich erreichbare Seiten.
|
||||||
|
try {
|
||||||
|
return await this.iconDiscovery.fetchPublicServiceIconBytes(link.url);
|
||||||
|
} catch {
|
||||||
|
throw new HttpException('Icon fetch failed', HttpStatus.BAD_GATEWAY);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -238,6 +238,57 @@ describe('IconDiscoveryService.fetchIconBytes', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('IconDiscoveryService.fetchPublicServiceIconBytes (quick-261001-hbi)', () => {
|
||||||
|
afterEach(() => {
|
||||||
|
vi.restoreAllMocks();
|
||||||
|
vi.unstubAllGlobals();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('fragt fuer eine oeffentliche Seite den Symbol-Dienst mit dem Hostnamen', async () => {
|
||||||
|
const fetchSpy = vi.fn().mockResolvedValue(mockResponse({ contentType: 'image/png' }));
|
||||||
|
vi.stubGlobal('fetch', fetchSpy);
|
||||||
|
|
||||||
|
const service = new IconDiscoveryService();
|
||||||
|
const result = await service.fetchPublicServiceIconBytes('http://8.8.8.8/start');
|
||||||
|
|
||||||
|
expect(fetchSpy).toHaveBeenCalledTimes(1);
|
||||||
|
expect(fetchSpy.mock.calls[0][0]).toBe('https://icons.duckduckgo.com/ip3/8.8.8.8.ico');
|
||||||
|
expect(result.contentType).toBe('image/png');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('fragt fuer eine interne Seite NICHT (Hostname verlaesst das Haus nicht)', async () => {
|
||||||
|
const fetchSpy = vi.fn();
|
||||||
|
vi.stubGlobal('fetch', fetchSpy);
|
||||||
|
|
||||||
|
const service = new IconDiscoveryService();
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
service.fetchPublicServiceIconBytes('https://docuvita.ctl.local/x'),
|
||||||
|
).rejects.toThrow(/not public/);
|
||||||
|
await expect(service.fetchPublicServiceIconBytes('http://192.168.1.5/')).rejects.toThrow(
|
||||||
|
/not public/,
|
||||||
|
);
|
||||||
|
expect(fetchSpy).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Dienst kennt kein Symbol (404) -> wirft', async () => {
|
||||||
|
vi.stubGlobal(
|
||||||
|
'fetch',
|
||||||
|
vi.fn().mockResolvedValue({
|
||||||
|
...mockResponse({ contentType: 'image/png' }),
|
||||||
|
ok: false,
|
||||||
|
status: 404,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const service = new IconDiscoveryService();
|
||||||
|
|
||||||
|
await expect(service.fetchPublicServiceIconBytes('http://8.8.8.8/')).rejects.toThrow(
|
||||||
|
/blocked or failed/,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('IconDiscoveryService.discoverFavoriteIconUrl (unchanged behaviour)', () => {
|
describe('IconDiscoveryService.discoverFavoriteIconUrl (unchanged behaviour)', () => {
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
vi.restoreAllMocks();
|
vi.restoreAllMocks();
|
||||||
|
|||||||
@@ -25,6 +25,18 @@ const MAX_REDIRECTS = 2;
|
|||||||
const MAX_HTML_CHARS = 200000;
|
const MAX_HTML_CHARS = 200000;
|
||||||
const MAX_ICON_BYTES = 1_000_000;
|
const MAX_ICON_BYTES = 1_000_000;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* quick-261001-hbi — oeffentlicher Symbol-Dienst als letzter Rueckfall. Manche
|
||||||
|
* Seiten setzen ihr Symbol erst per JavaScript (hosteurope.de: im HTML nur
|
||||||
|
* `<link rel="icon" href="data:;base64,=">`, `/favicon.ico` liefert eine
|
||||||
|
* HTML-Seite) — ohne Browser findet die Suche dort nichts. DuckDuckGo kennt
|
||||||
|
* das gerenderte Symbol und antwortet fuer Unbekanntes mit 404 (dann bleibt
|
||||||
|
* der Buchstabe). Gefragt wird NUR fuer oeffentlich erreichbare Adressen,
|
||||||
|
* damit interne Hostnamen (docuvita.ctl.local, private IPs) das Haus nie
|
||||||
|
* verlassen; der Dienst erfaehrt nur den Hostnamen.
|
||||||
|
*/
|
||||||
|
const PUBLIC_ICON_SERVICE = 'https://icons.duckduckgo.com/ip3/';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 260917-jdd — Ziel ist ein Bildchen, kein Geheimnis: selbstsignierte,
|
* 260917-jdd — Ziel ist ein Bildchen, kein Geheimnis: selbstsignierte,
|
||||||
* abgelaufene oder falsch benannte Zertifikate sollen das Symbol eines
|
* abgelaufene oder falsch benannte Zertifikate sollen das Symbol eines
|
||||||
@@ -426,6 +438,22 @@ export class IconDiscoveryService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* quick-261001-hbi: Symbol fuer die Seite `pageUrl` beim oeffentlichen
|
||||||
|
* Symbol-Dienst holen (siehe PUBLIC_ICON_SERVICE). Wirft, wenn die Seite
|
||||||
|
* nicht oeffentlich erreichbar ist (dann wird der Dienst NICHT gefragt) oder
|
||||||
|
* der Dienst kein Symbol kennt (404) — wie `fetchIconBytes`.
|
||||||
|
*/
|
||||||
|
async fetchPublicServiceIconBytes(
|
||||||
|
pageUrl: string,
|
||||||
|
): Promise<{ contentType: string; body: Buffer }> {
|
||||||
|
const page = new URL(normalizeUrl(pageUrl));
|
||||||
|
if (!(await isPublicHttpUrl(page))) {
|
||||||
|
throw new Error('Page is not public, icon service not asked');
|
||||||
|
}
|
||||||
|
return this.fetchIconBytes(`${PUBLIC_ICON_SERVICE}${page.hostname}.ico`);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetch the raw bytes of a stored icon URL, SSRF-guarded, for streaming
|
* Fetch the raw bytes of a stored icon URL, SSRF-guarded, for streaming
|
||||||
* back to the browser from Tessera's own origin (avoids Cross-Origin-
|
* back to the browser from Tessera's own origin (avoids Cross-Origin-
|
||||||
|
|||||||
@@ -65,14 +65,23 @@ export function ReminderFormModal({
|
|||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const titleRef = useRef<HTMLInputElement>(null);
|
const titleRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
|
// Fokus nur EINMAL beim Oeffnen (quick-261001-g68): `onClose` kommt aus der
|
||||||
|
// Kachel als neue Funktion bei jedem Neuzeichnen — die Kachel zeichnet alle
|
||||||
|
// 10 s neu (NOW_TICK_MS). Hing der Fokus mit am `onClose`-Effekt, sprang der
|
||||||
|
// Cursor beim Schreiben der Beschreibung immer wieder in den Titel.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
titleRef.current?.focus();
|
titleRef.current?.focus();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const onCloseRef = useRef(onClose);
|
||||||
|
onCloseRef.current = onClose;
|
||||||
|
useEffect(() => {
|
||||||
const onKey = (e: KeyboardEvent) => {
|
const onKey = (e: KeyboardEvent) => {
|
||||||
if (e.key === 'Escape') onClose();
|
if (e.key === 'Escape') onCloseRef.current();
|
||||||
};
|
};
|
||||||
document.addEventListener('keydown', onKey);
|
document.addEventListener('keydown', onKey);
|
||||||
return () => document.removeEventListener('keydown', onKey);
|
return () => document.removeEventListener('keydown', onKey);
|
||||||
}, [onClose]);
|
}, []);
|
||||||
|
|
||||||
const handleSubmit = async (e: React.FormEvent) => {
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|||||||
@@ -349,6 +349,32 @@ describe('ReminderWidget — Faelligkeit ohne Neuladen', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('ReminderWidget — Formular behaelt den Fokus (quick-261001-g68)', () => {
|
||||||
|
afterEach(() => {
|
||||||
|
vi.useRealTimers();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('der 10-s-Takt der Kachel holt den Cursor nicht aus der Beschreibung zurueck in den Titel', async () => {
|
||||||
|
vi.useFakeTimers();
|
||||||
|
vi.setSystemTime(new Date('2026-09-29T12:00:00.000Z'));
|
||||||
|
mockList.mockResolvedValue([]);
|
||||||
|
render(<ReminderWidget {...props} />);
|
||||||
|
await act(async () => {
|
||||||
|
await vi.advanceTimersByTimeAsync(0);
|
||||||
|
});
|
||||||
|
fireEvent.click(screen.getByText('reminder.add'));
|
||||||
|
expect(document.activeElement).toBe(screen.getByLabelText('reminder.titleLabel'));
|
||||||
|
|
||||||
|
const description = screen.getByLabelText('reminder.descriptionLabel');
|
||||||
|
description.focus();
|
||||||
|
fireEvent.change(description, { target: { value: 'Unterlagen mitnehmen' } });
|
||||||
|
await act(async () => {
|
||||||
|
await vi.advanceTimersByTimeAsync(30_000);
|
||||||
|
});
|
||||||
|
expect(document.activeElement).toBe(description);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('ReminderWidget — Spaeter erinnern kurz nach Mitternacht', () => {
|
describe('ReminderWidget — Spaeter erinnern kurz nach Mitternacht', () => {
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
vi.useRealTimers();
|
vi.useRealTimers();
|
||||||
|
|||||||
Reference in New Issue
Block a user