Files
logbuch/tests/components/blocks.test.tsx
T
Matthias Giesselmann b90ff252d1 Add Logbuch: project update blog with admin, media and API
Public archive with sidebar navigation, project pages, month archive,
search and entry pages with image blocks. Admin area for brands,
projects, post types, users, api clients, media and the entry editor
with drag and drop images, preview per audience, scheduling and
publish checks. Read and write API with bearer tokens, audience
scoping, idempotent creation, OpenAPI document and editorial guide.
Magic link login with configurable allowed domains, whole app behind
the session gate. 456 tests including design rule checks.
2026-07-31 21:33:42 +02:00

128 lines
4.0 KiB
TypeScript

import { describe, expect, it, vi } from 'vitest'
import { renderToStaticMarkup } from 'react-dom/server'
import type { ImageLoaderProps } from 'next/image'
import type { Media } from '~/data/schema'
vi.mock('next-intl', () => ({
useTranslations: () => (key: string) => key,
}))
vi.mock('next/image', () => ({
default: ({ loader, src, alt, sizes }: {
loader?: (args: ImageLoaderProps) => string
src: string
alt: string
sizes?: string
}) => <img src={loader ? loader({ src, width: 960, quality: 75 }) : src} alt={alt} data-sizes={sizes} />,
}))
const { blockRegistry } = await import('~/components/blocks/registry')
const base: Media = {
id: '0b6f0f4a-1c1f-4a51-9b2f-1d0a41f00001',
projectId: '0b6f0f4a-1c1f-4a51-9b2f-1d0a41f00009',
kind: 'image',
originalFilename: 'schema.png',
path: 'projekt/bild/original.png',
mime: 'image/png',
width: 2000,
height: 1125,
byteSize: 12345,
variants: [
{ width: 480, format: 'webp', path: 'projekt/bild/w480.webp' },
{ width: 960, format: 'webp', path: 'projekt/bild/w960.webp' },
],
variantError: null,
alt: 'Ein Schema',
caption: 'Bildunterschrift',
uploadedBy: null,
createdAt: new Date('2026-07-30T07:00:00Z'),
}
function media(...items: Media[]): Map<string, Media> {
return new Map(items.map(item => [item.id, item]))
}
function render(type: string, data: Record<string, unknown>, items: Map<string, Media>): string {
const Component = blockRegistry[type]
if (!Component) {
throw new Error(`kein Block für ${type}`)
}
return renderToStaticMarkup(<Component data={data} media={items} />)
}
describe('Blöcke ohne Medium', () => {
it('bleibt bei jedem Typ ohne Daten leer statt kaputt', () => {
for (const type of Object.keys(blockRegistry)) {
expect(render(type, {}, new Map()), type).toBe('')
}
})
it('verliert kein Bild, wenn die Kennung ins Leere zeigt', () => {
const missing = { mediaId: base.id, mediaIds: [base.id], beforeMediaId: base.id, afterMediaId: base.id }
for (const type of ['image', 'gallery', 'before_after', 'video']) {
expect(render(type, missing, new Map()), type).toBe('')
}
})
it('zeigt den Videoverweis auch ohne Medium', () => {
const markup = render('video', { url: 'https://example.test/film' }, new Map())
expect(markup).toContain('https://example.test/film')
})
})
describe('Blöcke mit Medium', () => {
it('holt die passende Fassung über den Medienweg', () => {
const markup = render('image', { mediaId: base.id }, media(base))
expect(markup).toContain('/api/v1/media/file/projekt/bild/w960.webp')
expect(markup).toContain('Ein Schema')
})
it('nimmt das Original, wenn keine Fassung erzeugt wurde', () => {
const original: Media = { ...base, variants: [] }
const markup = render('image', { mediaId: original.id }, media(original))
expect(markup).toContain('/api/v1/media/file/projekt/bild/original.png')
})
it('stellt die Galerie mit Vorschaubildern auf', () => {
const second: Media = { ...base, id: '0b6f0f4a-1c1f-4a51-9b2f-1d0a41f00002', caption: null }
const markup = render('gallery', { mediaIds: [base.id, second.id] }, media(base, second))
expect(markup.match(/<img/g)).toHaveLength(2)
expect(markup).toContain('position')
})
it('vergleicht vorher und nachher mit einem Schieberegler', () => {
const after: Media = { ...base, id: '0b6f0f4a-1c1f-4a51-9b2f-1d0a41f00003' }
const markup = render(
'before_after',
{ beforeMediaId: base.id, afterMediaId: after.id },
media(base, after),
)
expect(markup).toContain('type="range"')
expect(markup).toContain('clip-path')
})
it('zeigt nur eine Seite, wenn das Gegenstück fehlt', () => {
const markup = render('before_after', { beforeMediaId: base.id }, media(base))
expect(markup).not.toContain('type="range"')
expect(markup).toContain('before')
})
it('spielt ein Video als Video aus', () => {
const film: Media = { ...base, kind: 'video', mime: 'video/mp4', variants: [] }
const markup = render('video', { mediaId: film.id }, media(film))
expect(markup).toContain('<video')
expect(markup).toContain('/api/v1/media/file/projekt/bild/original.png')
})
})