feat(web): Einstellungen → Desktop-App zeigt in der App den verbundenen Server

- DesktopAppSettings rendert im Desktop-Client (useIsDesktopClient) einen Block "Verbunden mit: {origin}" plus Hinweis auf "Server-Adresse ändern…" im Infobereich-Menü; im Browser bleibt der Block weg
- Neue i18n-Schlüssel settings.desktop.connectedTo/changeHint in de.json und en.json
- Test 4 (Cookie tessera_desktop=1) und Test 5 (ohne Cookie) ergänzt; RED zuerst (Test 4 schlug auf der Zielbehauptung fehl), dann GREEN
- Alle 431 Web-Tests und type-check grün

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-17 14:52:47 +02:00
parent 29c132ecf3
commit 4c79874278
4 changed files with 51 additions and 4 deletions
@@ -1,5 +1,6 @@
import { cleanup, render, screen } from '@testing-library/react'; import { act, cleanup, render, screen } from '@testing-library/react';
import { afterEach, describe, expect, it, vi } from 'vitest'; import { afterEach, describe, expect, it, vi } from 'vitest';
import { DESKTOP_COOKIE_NAME } from '@/lib/desktop-client';
import { DesktopAppSettings } from './desktop-app-settings'; import { DesktopAppSettings } from './desktop-app-settings';
/** /**
@@ -34,9 +35,14 @@ vi.mock('@/lib/desktop', async (importOriginal) => {
}; };
}); });
function clearDesktopCookie() {
document.cookie = `${DESKTOP_COOKIE_NAME}=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/`;
}
afterEach(() => { afterEach(() => {
cleanup(); cleanup();
vi.clearAllMocks(); vi.clearAllMocks();
clearDesktopCookie();
}); });
const windowsFile = { const windowsFile = {
@@ -102,4 +108,24 @@ describe('DesktopAppSettings (18-03)', () => {
), ),
).toBeInTheDocument(); ).toBeInTheDocument();
}); });
it('Test 4 (im Desktop-Client): zeigt verbundenen Server und Änderungshinweis', async () => {
document.cookie = `${DESKTOP_COOKIE_NAME}=1; path=/`;
loadDesktopLatest.mockResolvedValue(null);
render(<DesktopAppSettings />);
expect(await screen.findByText('Verbunden mit: http://localhost:3000')).toBeInTheDocument();
expect(
screen.getByText('Ändern über das Tessera-Symbol im Infobereich → „Server-Adresse ändern…“'),
).toBeInTheDocument();
expect(screen.getByTestId('desktop-connected')).toBeInTheDocument();
});
it('Test 5 (im Browser): kein Block ohne Desktop-Cookie', async () => {
loadDesktopLatest.mockResolvedValue(null);
render(<DesktopAppSettings />);
await act(async () => {});
expect(screen.queryByTestId('desktop-connected')).toBeNull();
});
}); });
@@ -3,6 +3,7 @@
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { useLocale, useTranslations } from 'next-intl'; import { useLocale, useTranslations } from 'next-intl';
import { desktopDownloadUrl, type DesktopFileInfo, type DesktopLatestInfo, formatFileSize, loadDesktopLatest } from '@/lib/desktop'; import { desktopDownloadUrl, type DesktopFileInfo, type DesktopLatestInfo, formatFileSize, loadDesktopLatest } from '@/lib/desktop';
import { useIsDesktopClient } from '@/lib/desktop-client';
const BUTTON_CLASS = const BUTTON_CLASS =
'inline-flex items-center gap-2 rounded-md bg-primary px-4 py-2.5 text-sm font-medium text-primary-foreground hover:opacity-90'; 'inline-flex items-center gap-2 rounded-md bg-primary px-4 py-2.5 text-sm font-medium text-primary-foreground hover:opacity-90';
@@ -59,11 +60,17 @@ function DownloadButton({
/** /**
* Inhalt der Einstellungsseite "Desktop-App" (D-12): vier erklaerende * Inhalt der Einstellungsseite "Desktop-App" (D-12): vier erklaerende
* Saetze, dann Version/Downloads oder ein Hinweis, wenn der Server keine * Saetze, dann Version/Downloads oder ein Hinweis, wenn der Server keine
* Pakete traegt (`loadDesktopLatest()` liefert `null`). * Pakete traegt (`loadDesktopLatest()` liefert `null`). Im Desktop-Client
* (260917-jn2) zusaetzlich ein Block "Verbunden mit: {origin}" -- `isDesktop`
* ist im Server-HTML und im ersten Client-Render false (Hook mit
* `useEffect`), der Block rendert also erst nach der Hydration, weshalb
* `window.location.origin` hier direkt gelesen werden darf (kein
* Hydration-Fehler).
*/ */
export function DesktopAppSettings() { export function DesktopAppSettings() {
const t = useTranslations('settings'); const t = useTranslations('settings');
const locale = useLocale(); const locale = useLocale();
const isDesktop = useIsDesktopClient();
const [info, setInfo] = useState<DesktopLatestInfo | null | undefined>(undefined); const [info, setInfo] = useState<DesktopLatestInfo | null | undefined>(undefined);
useEffect(() => { useEffect(() => {
@@ -85,6 +92,16 @@ export function DesktopAppSettings() {
<p className="text-sm text-muted-foreground">{t('desktop.tray')}</p> <p className="text-sm text-muted-foreground">{t('desktop.tray')}</p>
<p className="text-sm text-muted-foreground">{t('desktop.update')}</p> <p className="text-sm text-muted-foreground">{t('desktop.update')}</p>
{isDesktop && (
<div
data-testid="desktop-connected"
className="mt-4 space-y-1 rounded border border-border bg-muted/30 p-3 text-sm"
>
<p className="text-foreground">{t('desktop.connectedTo', { origin: window.location.origin })}</p>
<p className="text-muted-foreground">{t('desktop.changeHint')}</p>
</div>
)}
{info === null && <p className="mt-4 text-sm text-muted-foreground">{t('desktop.unavailable')}</p>} {info === null && <p className="mt-4 text-sm text-muted-foreground">{t('desktop.unavailable')}</p>}
{info && ( {info && (
+3 -1
View File
@@ -169,7 +169,9 @@
"downloadWindows": "Für Windows herunterladen", "downloadWindows": "Für Windows herunterladen",
"downloadLinux": "Für Linux herunterladen", "downloadLinux": "Für Linux herunterladen",
"fileInfo": "{name} · {size}", "fileInfo": "{name} · {size}",
"unavailable": "Auf diesem Server sind derzeit keine Desktop-Pakete hinterlegt." "unavailable": "Auf diesem Server sind derzeit keine Desktop-Pakete hinterlegt.",
"connectedTo": "Verbunden mit: {origin}",
"changeHint": "Ändern über das Tessera-Symbol im Infobereich → „Server-Adresse ändern…“"
}, },
"sourceDeleteConfirm": "Möchten Sie diese Kalenderquelle wirklich löschen?", "sourceDeleteConfirm": "Möchten Sie diese Kalenderquelle wirklich löschen?",
"sourceDeleteCta": "Quelle löschen", "sourceDeleteCta": "Quelle löschen",
+3 -1
View File
@@ -169,7 +169,9 @@
"downloadWindows": "Download for Windows", "downloadWindows": "Download for Windows",
"downloadLinux": "Download for Linux", "downloadLinux": "Download for Linux",
"fileInfo": "{name} · {size}", "fileInfo": "{name} · {size}",
"unavailable": "No desktop packages are available on this server yet." "unavailable": "No desktop packages are available on this server yet.",
"connectedTo": "Connected to: {origin}",
"changeHint": "To change it, use the Tessera icon in the notification area → “Change server address…”"
}, },
"sourceDeleteConfirm": "Are you sure you want to delete this calendar source?", "sourceDeleteConfirm": "Are you sure you want to delete this calendar source?",
"sourceDeleteCta": "Delete source", "sourceDeleteCta": "Delete source",