feat(18-03): Task 1 — Fetch-Helfer und Download-Link auf der Anmeldeseite
- apps/web/src/lib/desktop.ts: loadDesktopLatest (memoisiert, still bei Fehler), desktopDownloadUrl, formatFileSize (lokalisierte MB-Werte) - DesktopDownloadLinks: unauffaelliger Link-Block, rendert nichts ohne Daten; Windows fuehrt, Linux als Kurzlink wenn beide Pakete vorliegen - Anmeldeseite bindet den Block nach dem Formular ein (D-12) - de/en: auth.desktopDownload.* ergaenzt Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,97 @@
|
||||
import { cleanup, render, screen } from '@testing-library/react';
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
import { DesktopDownloadLinks } from './desktop-download-links';
|
||||
|
||||
/**
|
||||
* desktop-download-links.test — Link-Block auf der Anmeldeseite (18-03).
|
||||
* next-intl-Mock nach dem Muster in widget-settings-panel.test.tsx
|
||||
* (de.json-gestuetzt), `@/lib/desktop` gemockt mit steuerbarem
|
||||
* `loadDesktopLatest`; `desktopDownloadUrl` wird echt durchgereicht.
|
||||
*/
|
||||
vi.mock('next-intl', async () => {
|
||||
const messages = (await import('@/messages/de.json')).default as Record<string, unknown>;
|
||||
const lookup = (path: string): string | undefined =>
|
||||
path.split('.').reduce<unknown>((o, k) => (o && typeof o === 'object' ? (o as any)[k] : undefined), messages) as
|
||||
| string
|
||||
| undefined;
|
||||
return {
|
||||
useTranslations:
|
||||
(ns?: string) =>
|
||||
(key: string, values?: Record<string, unknown>) => {
|
||||
const raw = lookup(ns ? `${ns}.${key}` : key) ?? key;
|
||||
return values ? raw.replace(/\{(\w+)\}/g, (_: string, n: string) => String(values[n] ?? '')) : raw;
|
||||
},
|
||||
useLocale: () => 'de',
|
||||
};
|
||||
});
|
||||
|
||||
const { loadDesktopLatest } = vi.hoisted(() => ({ loadDesktopLatest: vi.fn() }));
|
||||
|
||||
vi.mock('@/lib/desktop', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('@/lib/desktop')>();
|
||||
return {
|
||||
...actual,
|
||||
loadDesktopLatest,
|
||||
};
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
const windowsFile = {
|
||||
name: 'Tessera-Setup-1.1.0.exe',
|
||||
size: 6123456,
|
||||
sha256: 'x',
|
||||
url: '/desktop/download/windows',
|
||||
};
|
||||
const linuxFile = {
|
||||
name: 'Tessera-1.1.0.AppImage',
|
||||
size: 106461688,
|
||||
sha256: 'y',
|
||||
url: '/desktop/download/linux',
|
||||
};
|
||||
|
||||
describe('DesktopDownloadLinks (18-03)', () => {
|
||||
it('Test 1 (keine Daten): rendert nichts, solange null zurueckkommt', async () => {
|
||||
loadDesktopLatest.mockResolvedValue(null);
|
||||
const { container } = render(<DesktopDownloadLinks />);
|
||||
await vi.waitFor(() => expect(loadDesktopLatest).toHaveBeenCalled());
|
||||
expect(container.firstChild).toBeNull();
|
||||
});
|
||||
|
||||
it('Test 2 (beide Plattformen): Windows-Hauptlink, Linux-Kurzlink, Version', async () => {
|
||||
loadDesktopLatest.mockResolvedValue({
|
||||
version: '1.1.0',
|
||||
channel: 'beta',
|
||||
commit: 'abc1234',
|
||||
buildTime: 'x',
|
||||
files: { windows: windowsFile, linux: linuxFile },
|
||||
});
|
||||
render(<DesktopDownloadLinks />);
|
||||
|
||||
const windowsLink = await screen.findByText('Desktop-App herunterladen (Windows)');
|
||||
expect(windowsLink.closest('a')).toHaveAttribute('href', 'http://localhost:3001/desktop/download/windows');
|
||||
|
||||
const linuxLink = screen.getByText('Linux-Version');
|
||||
expect(linuxLink.closest('a')).toHaveAttribute('href', 'http://localhost:3001/desktop/download/linux');
|
||||
|
||||
expect(screen.getByText('Version 1.1.0')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Test 3 (nur Linux): genau ein Link mit dem Linux-Text', async () => {
|
||||
loadDesktopLatest.mockResolvedValue({
|
||||
version: '1.1.0',
|
||||
channel: 'beta',
|
||||
commit: 'abc1234',
|
||||
buildTime: 'x',
|
||||
files: { linux: linuxFile },
|
||||
});
|
||||
render(<DesktopDownloadLinks />);
|
||||
|
||||
const linuxLink = await screen.findByText('Desktop-App herunterladen (Linux)');
|
||||
expect(linuxLink.closest('a')).toHaveAttribute('href', 'http://localhost:3001/desktop/download/linux');
|
||||
expect(screen.queryByText('Linux-Version')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,66 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { desktopDownloadUrl, type DesktopLatestInfo, loadDesktopLatest } from '@/lib/desktop';
|
||||
|
||||
/**
|
||||
* Unauffaelliger Download-Link auf der Anmeldeseite (D-12): erscheint nur,
|
||||
* wenn `/desktop/latest` antwortet — kein Fehlertext, kein Ladeanzeiger,
|
||||
* nichts, solange nichts geladen ist. Windows fuehrt (Hauptlink), Linux
|
||||
* folgt als kleiner zweiter Link, wenn beide Pakete vorliegen.
|
||||
*/
|
||||
export function DesktopDownloadLinks() {
|
||||
const t = useTranslations('auth');
|
||||
const [info, setInfo] = useState<DesktopLatestInfo | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
let active = true;
|
||||
loadDesktopLatest().then((data) => {
|
||||
if (active) setInfo(data);
|
||||
});
|
||||
return () => {
|
||||
active = false;
|
||||
};
|
||||
}, []);
|
||||
|
||||
const files = info?.files;
|
||||
const windows = files?.windows;
|
||||
const linux = files?.linux;
|
||||
if (!info || (!windows && !linux)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const primary = windows ?? linux;
|
||||
const primaryLabel = windows ? t('desktopDownload.windows') : t('desktopDownload.linux');
|
||||
const showLinuxShort = Boolean(windows && linux);
|
||||
|
||||
return (
|
||||
<div className="text-center text-sm text-muted-foreground">
|
||||
<div>
|
||||
{primary && (
|
||||
<a
|
||||
href={desktopDownloadUrl(primary)}
|
||||
download
|
||||
className="hover:text-foreground underline-offset-4 hover:underline"
|
||||
>
|
||||
{primaryLabel}
|
||||
</a>
|
||||
)}
|
||||
{showLinuxShort && linux && (
|
||||
<>
|
||||
{' · '}
|
||||
<a
|
||||
href={desktopDownloadUrl(linux)}
|
||||
download
|
||||
className="hover:text-foreground underline-offset-4 hover:underline"
|
||||
>
|
||||
{t('desktopDownload.linuxShort')}
|
||||
</a>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<div className="text-xs">{t('desktopDownload.version', { version: info.version })}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user