Web-Anwendung: Design-Tokens, Texte, Gesundheitsendpunkt
Design-Tokens aus docs/10-design-system.md als CSS-Variablen: Grundpalette, die sechs Klassifizierungsfarben, Eckenradius durchgehend 2 px, keine Schatten ausser dem Fokusring. Der Schwärzungsbalken mit dem einmaligen Durchwischen respektiert prefers-reduced-motion. Vier Tests halten die Tokens gegen das Dokument. IBM Plex selbst gehostet als woff2, latin und latin-ext getrennt über unicode-range, font-display: swap, immutable-Cache. Alle Oberflächentexte in lib/i18n/de.ts. Ein Test durchsucht app/ und components/ nach Umlauten im JSX und schlägt fehl, wenn dort deutscher Text steht. Er hat beim ersten Lauf gleich einen Verstoss gefunden. /api/health prüft Datenbank, Discord-Gateway und ClamAV. Die Gesamtbewertung fällt nur auf "down", wenn die Datenbank weg ist: die anderen Dienste haben eigene Healthchecks im Stack, und ein ausgefallener Virenscanner soll nicht den Web-Container als tot markieren. Strikte CSP mit Nonce, kein unsafe-inline für Skripte. Die erste Route liegt bei 100 kB JavaScript nach Kompression, Budget sind 120 kB.
This commit is contained in:
@@ -0,0 +1,35 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { join } from 'node:path'
|
||||
|
||||
const css = readFileSync(join(process.cwd(), 'app/globals.css'), 'utf8')
|
||||
|
||||
describe('Design-Tokens', () => {
|
||||
it('enthaelt die Grundpalette aus dem Design-System', () => {
|
||||
for (const hex of ['#15181a', '#1d2124', '#262b2f', '#3a4147', '#e4e7e4', '#939c99', '#7a97a6']) {
|
||||
expect(css).toContain(hex)
|
||||
}
|
||||
})
|
||||
|
||||
it('enthaelt alle sechs Klassifizierungsfarben', () => {
|
||||
for (const hex of ['#8a918d', '#6d8a9c', '#4e8f72', '#b08637', '#b4543c', '#7e5e96']) {
|
||||
expect(css).toContain(hex)
|
||||
}
|
||||
})
|
||||
|
||||
it('setzt den Eckenradius durchgehend auf 2 px', () => {
|
||||
const radii = [...css.matchAll(/--radius-[\w-]+:\s*([^;]+);/g)].map((m) => m[1]!.trim())
|
||||
expect(radii.length).toBeGreaterThan(0)
|
||||
for (const value of radii) expect(['0px', '2px']).toContain(value)
|
||||
})
|
||||
|
||||
it('kennt keine Schatten ausser dem Fokusring', () => {
|
||||
const shadows = [...css.matchAll(/--shadow-[\w-]+:\s*([^;]+);/g)].map((m) => m[1]!.trim())
|
||||
expect(shadows.length).toBeGreaterThan(0)
|
||||
for (const value of shadows) expect(value).toBe('none')
|
||||
})
|
||||
|
||||
it('respektiert prefers-reduced-motion bei der Schwaerzung', () => {
|
||||
expect(css).toMatch(/@media \(prefers-reduced-motion: reduce\)/)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,33 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
|
||||
vi.mock('@/lib/db', () => ({
|
||||
db: {
|
||||
$queryRaw: vi.fn(async () => [{ '?column?': 1 }]),
|
||||
serviceHeartbeat: { findUnique: vi.fn(async () => ({ service: 'worker', status: 'up', detail: null, at: new Date() })) },
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock('@/lib/env', () => ({
|
||||
getEnv: () => ({ CLAMAV_SOCKET: '/var/run/clamav/clamd.sock' }),
|
||||
}))
|
||||
|
||||
vi.mock('@/lib/health/clamav', () => ({ pingClamav: vi.fn(async () => true) }))
|
||||
|
||||
describe('collectHealth', () => {
|
||||
it('meldet betriebsbereit, wenn alle drei Abhaengigkeiten antworten', async () => {
|
||||
const { collectHealth } = await import('@/lib/health/check')
|
||||
const report = await collectHealth()
|
||||
expect(report.status).toBe('up')
|
||||
expect(report.components.database.status).toBe('up')
|
||||
expect(report.components.discordGateway.status).toBe('up')
|
||||
expect(report.components.clamav.status).toBe('up')
|
||||
})
|
||||
|
||||
it('bleibt nur eingeschraenkt, wenn allein die Virenpruefung ausfaellt', async () => {
|
||||
const clamav = await import('@/lib/health/clamav')
|
||||
vi.mocked(clamav.pingClamav).mockResolvedValueOnce(false)
|
||||
const { collectHealth } = await import('@/lib/health/check')
|
||||
const report = await collectHealth()
|
||||
expect(report.status).toBe('degraded')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,47 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { readFileSync, readdirSync, statSync } from 'node:fs'
|
||||
import { join } from 'node:path'
|
||||
import { de } from '@/lib/i18n/de'
|
||||
|
||||
describe('Sprachkonvention', () => {
|
||||
it('kennt alle sechs Freigabestufen', () => {
|
||||
for (let level = 0; level <= 5; level++) {
|
||||
expect(de.clearance.names[level]).toBeTypeOf('string')
|
||||
}
|
||||
expect(de.clearance.names[3]).toBe('Geheim')
|
||||
})
|
||||
|
||||
it('verzichtet auf Ausrufezeichen und Entschuldigungen', () => {
|
||||
const flat = JSON.stringify(de)
|
||||
expect(flat).not.toMatch(/!/)
|
||||
expect(flat.toLowerCase()).not.toMatch(/erfolgreich|entschuldigung/)
|
||||
})
|
||||
|
||||
it('haelt deutsche Oberflaechentexte aus dem JSX heraus', () => {
|
||||
// Umlaute und Eszett sind der billigste verlaessliche Marker fuer einen
|
||||
// deutschen Text. Alles Sichtbare gehoert nach lib/i18n/de.ts, siehe CLAUDE.md.
|
||||
const offenders: string[] = []
|
||||
const roots = ['app', 'components']
|
||||
|
||||
function walk(dir: string) {
|
||||
for (const name of readdirSync(dir)) {
|
||||
const path = join(dir, name)
|
||||
if (statSync(path).isDirectory()) {
|
||||
walk(path)
|
||||
continue
|
||||
}
|
||||
if (!name.endsWith('.tsx')) continue
|
||||
const source = readFileSync(path, 'utf8')
|
||||
for (const [index, line] of source.split('\n').entries()) {
|
||||
const withoutComments = line.replace(/\/\/.*$/, '').replace(/\/\*.*?\*\//g, '')
|
||||
if (/[äöüÄÖÜß]/.test(withoutComments)) {
|
||||
offenders.push(`${path}:${index + 1}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const root of roots) walk(join(process.cwd(), root))
|
||||
expect(offenders).toEqual([])
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,3 @@
|
||||
import '@testing-library/dom'
|
||||
|
||||
process.env.TZ = 'Europe/Berlin'
|
||||
Reference in New Issue
Block a user