feat(nextcloud-status): Modul mit Statusabruf, Versionsbewertung und Kachelansicht
- NextcloudInstance mit Zeilenschutz, Migration 20261002150000 - Bewertung (Ampel) als reine Funktion, status.php-Abruf mit Grenzen, endoflife.date-Zwischenspeicher - API: Liste mit Bewertung, Anlegen und Einzelprüfung nur mit Verwalten - Modulseite mit Kacheln, Registrierung in Seitenleiste, Marktplatz und Symbolen Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,170 @@
|
||||
import { cleanup, render as rtlRender, screen, waitFor, within } from '@testing-library/react';
|
||||
import { NextIntlClientProvider } from 'next-intl';
|
||||
import type { ReactElement } from 'react';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import type { NextcloudInstance, NextcloudList } from '@/lib/nextcloud-status-api';
|
||||
import de from '@/messages/de.json';
|
||||
import NextcloudStatusPage from './page';
|
||||
|
||||
function render(ui: ReactElement) {
|
||||
return rtlRender(
|
||||
<NextIntlClientProvider locale="de" messages={de} timeZone="Europe/Berlin">
|
||||
{ui}
|
||||
</NextIntlClientProvider>,
|
||||
);
|
||||
}
|
||||
|
||||
const mockListInstances = vi.fn();
|
||||
|
||||
vi.mock('@/lib/nextcloud-status-api', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('@/lib/nextcloud-status-api')>();
|
||||
return { ...actual, listInstances: (...args: unknown[]) => mockListInstances(...args) };
|
||||
});
|
||||
|
||||
function makeInstance(
|
||||
over: Partial<NextcloudInstance> & { id: string; customerName: string },
|
||||
): NextcloudInstance {
|
||||
return {
|
||||
baseUrl: `https://${over.id}.example.de`,
|
||||
logoUrl: null,
|
||||
hasUploadedLogo: false,
|
||||
logoVersion: 0,
|
||||
status: {
|
||||
checkedAt: new Date().toISOString(),
|
||||
reachable: true,
|
||||
maintenance: false,
|
||||
needsDbUpgrade: false,
|
||||
versionString: '35.0.1',
|
||||
edition: null,
|
||||
errorKind: null,
|
||||
errorDetail: null,
|
||||
},
|
||||
rating: { level: 'green', reason: 'current', updateTo: null, eolDate: null, cycle: 35 },
|
||||
...over,
|
||||
};
|
||||
}
|
||||
|
||||
const LIST: NextcloudList = {
|
||||
reference: { newestVersion: '35.0.1', fetchedAt: '2026-10-02T10:00:00.000Z' },
|
||||
instances: [
|
||||
makeInstance({ id: 'a', customerName: 'Kunde Grün' }),
|
||||
makeInstance({
|
||||
id: 'b',
|
||||
customerName: 'Kunde Gelb',
|
||||
status: {
|
||||
checkedAt: new Date().toISOString(),
|
||||
reachable: true,
|
||||
maintenance: false,
|
||||
needsDbUpgrade: false,
|
||||
versionString: '34.0.3',
|
||||
edition: null,
|
||||
errorKind: null,
|
||||
errorDetail: null,
|
||||
},
|
||||
rating: {
|
||||
level: 'yellow',
|
||||
reason: 'update-available',
|
||||
updateTo: '34.0.4',
|
||||
eolDate: '2027-06-30',
|
||||
cycle: 34,
|
||||
},
|
||||
}),
|
||||
makeInstance({
|
||||
id: 'c',
|
||||
customerName: 'Kunde Rot',
|
||||
status: {
|
||||
checkedAt: new Date().toISOString(),
|
||||
reachable: false,
|
||||
maintenance: null,
|
||||
needsDbUpgrade: null,
|
||||
versionString: null,
|
||||
edition: null,
|
||||
errorKind: 'timeout',
|
||||
errorDetail: null,
|
||||
},
|
||||
rating: { level: 'red', reason: 'unreachable', updateTo: null, eolDate: null, cycle: null },
|
||||
}),
|
||||
makeInstance({
|
||||
id: 'd',
|
||||
customerName: 'Kunde Grau',
|
||||
rating: {
|
||||
level: 'unknown',
|
||||
reason: 'no-reference',
|
||||
updateTo: null,
|
||||
eolDate: null,
|
||||
cycle: 35,
|
||||
},
|
||||
}),
|
||||
],
|
||||
};
|
||||
|
||||
describe('NextcloudStatusPage', () => {
|
||||
beforeEach(() => {
|
||||
mockListInstances.mockReset();
|
||||
});
|
||||
afterEach(() => cleanup());
|
||||
|
||||
it('zeigt je Cloud eine Kachel mit Name, Version, Klartext und Verweis', async () => {
|
||||
mockListInstances.mockResolvedValue(LIST);
|
||||
render(<NextcloudStatusPage />);
|
||||
|
||||
const tiles = await screen.findAllByTestId('cloud-tile');
|
||||
expect(tiles).toHaveLength(4);
|
||||
|
||||
const green = tiles.find((t) => t.textContent?.includes('Kunde Grün'));
|
||||
expect(green).toBeDefined();
|
||||
expect(within(green as HTMLElement).getByText('Aktuell')).toBeInTheDocument();
|
||||
expect(green).toHaveAttribute('data-level', 'green');
|
||||
|
||||
expect(screen.getByText('Update auf 34.0.4 verfügbar')).toBeInTheDocument();
|
||||
expect(screen.getByText('Nicht erreichbar')).toBeInTheDocument();
|
||||
expect(screen.getByText('Bewertung nicht möglich')).toBeInTheDocument();
|
||||
expect(screen.getByText('34.0.3')).toBeInTheDocument();
|
||||
|
||||
const link = screen.getByRole('link', { name: 'https://a.example.de' });
|
||||
expect(link).toHaveAttribute('target', '_blank');
|
||||
expect(link).toHaveAttribute('rel', 'noopener noreferrer');
|
||||
expect(link).toHaveAttribute('href', 'https://a.example.de');
|
||||
});
|
||||
|
||||
it('nennt die neueste Nextcloud-Version', async () => {
|
||||
mockListInstances.mockResolvedValue(LIST);
|
||||
render(<NextcloudStatusPage />);
|
||||
expect(await screen.findByTestId('newest-version')).toHaveTextContent(
|
||||
'Neueste Nextcloud-Version: 35.0.1',
|
||||
);
|
||||
});
|
||||
|
||||
it('weist ohne Vergleichsdaten darauf hin', async () => {
|
||||
mockListInstances.mockResolvedValue({
|
||||
...LIST,
|
||||
reference: { newestVersion: null, fetchedAt: null },
|
||||
});
|
||||
render(<NextcloudStatusPage />);
|
||||
expect(await screen.findByTestId('newest-version')).toHaveTextContent(
|
||||
'Versionsdaten derzeit nicht verfügbar',
|
||||
);
|
||||
});
|
||||
|
||||
it('zeigt den Leerzustand ohne Clouds', async () => {
|
||||
mockListInstances.mockResolvedValue({ ...LIST, instances: [] });
|
||||
render(<NextcloudStatusPage />);
|
||||
expect(await screen.findByText('Es sind noch keine Clouds eingetragen.')).toBeInTheDocument();
|
||||
expect(screen.queryByTestId('cloud-tile')).toBeNull();
|
||||
});
|
||||
|
||||
it('zeigt einen Hinweis, wenn das Laden fehlschlägt', async () => {
|
||||
mockListInstances.mockRejectedValue(new Error('boom'));
|
||||
render(<NextcloudStatusPage />);
|
||||
await waitFor(() =>
|
||||
expect(screen.getByText(/konnten nicht geladen werden/)).toBeInTheDocument(),
|
||||
);
|
||||
});
|
||||
|
||||
it('verwendet ohne Logo Initialen', async () => {
|
||||
mockListInstances.mockResolvedValue({ ...LIST, instances: [LIST.instances[0]] });
|
||||
render(<NextcloudStatusPage />);
|
||||
const tile = await screen.findByTestId('cloud-tile');
|
||||
expect(within(tile).getByText('KG')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user