b90ff252d1
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.
128 lines
4.0 KiB
TypeScript
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')
|
|
})
|
|
})
|