feat(cms): add network page video upload
This commit is contained in:
@@ -30,7 +30,7 @@ This inventory records the frontend or platform owner for every retained `Pages`
|
||||
| `mitgliedWerden.*` | `src/spa/pages/MitgliedWerden.tsx` and `src/spa/components/membership/MembershipWizard.tsx`. |
|
||||
| `preistraegerIndex.*` | `src/spa/pages/Preistraeger.tsx`; winner records remain collection-owned. |
|
||||
| `pressIndex.hero.*`, `pressIndex.events.*`, `pressIndex.news.*`, `pressIndex.downloads.*` | `src/spa/pages/Press.tsx`; event/article records remain collection-owned, while missing-field labels and images are presentation fallbacks only. |
|
||||
| `netzwerk.hero.*`, `netzwerk.patronage.*`, `netzwerk.juryIntro.*`, `netzwerk.expertise.*`, `netzwerk.sponsoring.*` | `src/spa/pages/Netzwerk.tsx`. |
|
||||
| `netzwerk.hero.*`, `netzwerk.patronage.*`, `netzwerk.juryIntro.*`, `netzwerk.expertise.*`, `netzwerk.sponsoring.*` | `src/spa/pages/Netzwerk.tsx`. The page-owned `netzwerk.patronage.greetingVideo` Media relationship and its adjacent button/modal copy control only the Ilse Aigner greeting on `/netzwerk`; they never inherit `home.videoModal.*`. |
|
||||
| `netzwerk.jury.{eyebrow,heading,description,chairBadge,members}` | Jury heading and authoritative rendered relationship selection in `src/spa/pages/Netzwerk.tsx`. |
|
||||
| `netzwerk.partners.{eyebrow,heading,description,detailLabel,tiers,modal}` | Partner section, tier labels, and modal copy in `src/spa/pages/Netzwerk.tsx`; partner records remain collection-owned. |
|
||||
| `netzwerk.sponsoringForm.*` | The two visible form steps, validation, success state, and footer copy in `src/spa/components/forms/SponsoringForm.tsx`. |
|
||||
|
||||
@@ -100,6 +100,24 @@ export const netzwerkFields: Field[] = [
|
||||
text('greetingTitleLine2', 'Greeting title line 2', netzwerkContent.patronage.greetingTitleLine2),
|
||||
textarea('greetingQuote', 'Greeting quote', netzwerkContent.patronage.greetingQuote),
|
||||
text('greetingAttribution', 'Greeting attribution', netzwerkContent.patronage.greetingAttribution),
|
||||
{
|
||||
name: 'greetingVideo',
|
||||
type: 'upload',
|
||||
relationTo: 'media',
|
||||
label: 'Greeting video',
|
||||
filterOptions: {
|
||||
mimeType: {
|
||||
contains: 'video',
|
||||
},
|
||||
},
|
||||
admin: {
|
||||
description: 'Choose the Ilse Aigner greeting video. If empty, the video action is hidden.',
|
||||
},
|
||||
},
|
||||
text('greetingVideoButtonLabel', 'Video button label', netzwerkContent.patronage.greetingVideoButtonLabel),
|
||||
text('greetingVideoTitle', 'Video title', netzwerkContent.patronage.greetingVideoTitle),
|
||||
text('greetingVideoDescription', 'Video description', netzwerkContent.patronage.greetingVideoDescription),
|
||||
text('greetingVideoCloseLabel', 'Video close button label', netzwerkContent.patronage.greetingVideoCloseLabel),
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
169
src/migrations/20260731_170000_netzwerk_greeting_video.ts
Normal file
169
src/migrations/20260731_170000_netzwerk_greeting_video.ts
Normal file
@@ -0,0 +1,169 @@
|
||||
import { type MigrateDownArgs, type MigrateUpArgs, sql } from '@payloadcms/db-sqlite'
|
||||
|
||||
import { netzwerkContent } from '@/spa/netzwerkContent'
|
||||
|
||||
type MigrationDB = MigrateUpArgs['db']
|
||||
|
||||
const ident = (value: string) => `\`${value.replace(/`/g, '``')}\``
|
||||
const literal = (value: string) => `'${value.replace(/'/g, "''")}'`
|
||||
|
||||
const liveVideoColumn = 'netzwerk_patronage_greeting_video_id'
|
||||
const versionVideoColumn = `version_${liveVideoColumn}`
|
||||
const liveVideoIndex = 'pages_netzwerk_patronage_netzwerk_patronage_greeting_vid_idx'
|
||||
const versionVideoIndex = '_pages_v_version_netzwerk_patronage_version_netzwerk_pat_idx'
|
||||
|
||||
const textColumns = [
|
||||
['netzwerk_patronage_greeting_video_button_label', netzwerkContent.patronage.greetingVideoButtonLabel],
|
||||
['netzwerk_patronage_greeting_video_title', netzwerkContent.patronage.greetingVideoTitle],
|
||||
['netzwerk_patronage_greeting_video_description', netzwerkContent.patronage.greetingVideoDescription],
|
||||
['netzwerk_patronage_greeting_video_close_label', netzwerkContent.patronage.greetingVideoCloseLabel],
|
||||
] as const
|
||||
|
||||
async function tableExists(db: MigrationDB, tableName: string) {
|
||||
const rows = (await db.all(
|
||||
sql.raw(`SELECT name FROM sqlite_master WHERE type = 'table' AND name = ${literal(tableName)}`),
|
||||
)) as Array<{ name: string }>
|
||||
return rows.length > 0
|
||||
}
|
||||
|
||||
async function columnExists(db: MigrationDB, tableName: string, columnName: string) {
|
||||
if (!(await tableExists(db, tableName))) return false
|
||||
const columns = (await db.all(sql.raw(`PRAGMA table_info(${ident(tableName)})`))) as Array<{ name: string }>
|
||||
return columns.some((column) => column.name === columnName)
|
||||
}
|
||||
|
||||
async function columnNames(db: MigrationDB, tableName: string) {
|
||||
if (!(await tableExists(db, tableName))) return []
|
||||
const columns = (await db.all(sql.raw(`PRAGMA table_info(${ident(tableName)})`))) as Array<{ name: string }>
|
||||
return columns.map((column) => column.name)
|
||||
}
|
||||
|
||||
async function addColumn(db: MigrationDB, tableName: string, columnName: string, definition: string) {
|
||||
if (await columnExists(db, tableName, columnName)) return
|
||||
await db.run(sql.raw(`ALTER TABLE ${ident(tableName)} ADD ${ident(columnName)} ${definition};`))
|
||||
}
|
||||
|
||||
function splitDefinitions(value: string) {
|
||||
const definitions: string[] = []
|
||||
let current = ''
|
||||
let depth = 0
|
||||
let quote = ''
|
||||
|
||||
for (let index = 0; index < value.length; index += 1) {
|
||||
const character = value[index]
|
||||
const previous = value[index - 1]
|
||||
|
||||
if (quote) {
|
||||
current += character
|
||||
if (character === quote && previous !== '\\') quote = ''
|
||||
continue
|
||||
}
|
||||
if (character === '`' || character === '"' || character === "'") {
|
||||
quote = character
|
||||
current += character
|
||||
continue
|
||||
}
|
||||
if (character === '(') depth += 1
|
||||
if (character === ')') depth -= 1
|
||||
if (character === ',' && depth === 0) {
|
||||
definitions.push(current.trim())
|
||||
current = ''
|
||||
continue
|
||||
}
|
||||
current += character
|
||||
}
|
||||
|
||||
if (current.trim()) definitions.push(current.trim())
|
||||
return definitions
|
||||
}
|
||||
|
||||
function definitionColumnName(definition: string) {
|
||||
const match = definition.trim().match(/^(?:`([^`]+)`|"([^"]+)"|\[([^\]]+)\]|([^\s]+))/)
|
||||
return match?.[1] || match?.[2] || match?.[3] || match?.[4] || ''
|
||||
}
|
||||
|
||||
function referencesColumn(definition: string, column: string) {
|
||||
return [ident(column), `"${column}"`, `[${column}]`].some((value) => definition.includes(value))
|
||||
}
|
||||
|
||||
async function rebuildTableWithoutColumns(db: MigrationDB, tableName: string, removedColumns: readonly string[]) {
|
||||
const schemaRows = (await db.all(
|
||||
sql.raw(`SELECT sql FROM sqlite_master WHERE type = 'table' AND name = ${literal(tableName)}`),
|
||||
)) as Array<{ sql: string }>
|
||||
const schema = schemaRows[0]?.sql
|
||||
if (!schema) return
|
||||
|
||||
const presentColumns = await columnNames(db, tableName)
|
||||
const removed = removedColumns.filter((column) => presentColumns.includes(column))
|
||||
if (!removed.length) return
|
||||
|
||||
const open = schema.indexOf('(')
|
||||
const close = schema.lastIndexOf(')')
|
||||
if (open < 0 || close < open) throw new Error(`Could not parse schema for ${tableName}`)
|
||||
|
||||
const keptDefinitions = splitDefinitions(schema.slice(open + 1, close)).filter((definition) => {
|
||||
const columnName = definitionColumnName(definition)
|
||||
const isConstraint = /^(CONSTRAINT|FOREIGN|PRIMARY|UNIQUE|CHECK)\b/i.test(columnName)
|
||||
if (isConstraint) return !removed.some((column) => referencesColumn(definition, column))
|
||||
return !removed.includes(columnName)
|
||||
})
|
||||
const keptColumns = presentColumns.filter((column) => !removed.includes(column))
|
||||
const indexRows = (await db.all(
|
||||
sql.raw(`SELECT sql FROM sqlite_master WHERE type = 'index' AND tbl_name = ${literal(tableName)} AND sql IS NOT NULL`),
|
||||
)) as Array<{ sql: string }>
|
||||
const keptIndexes = indexRows
|
||||
.map((row) => row.sql)
|
||||
.filter((indexSql) => !removed.some((column) => referencesColumn(indexSql, column)))
|
||||
const temporaryTable = `__task5_${tableName}`
|
||||
const columnList = keptColumns.map(ident).join(', ')
|
||||
|
||||
await db.run(sql.raw(`DROP TABLE IF EXISTS ${ident(temporaryTable)};`))
|
||||
await db.run(sql.raw(`CREATE TABLE ${ident(temporaryTable)} (${keptDefinitions.join(',\n')})${schema.slice(close + 1)};`))
|
||||
await db.run(sql.raw(`INSERT INTO ${ident(temporaryTable)} (${columnList}) SELECT ${columnList} FROM ${ident(tableName)};`))
|
||||
await db.run(sql.raw(`DROP TABLE ${ident(tableName)};`))
|
||||
await db.run(sql.raw(`ALTER TABLE ${ident(temporaryTable)} RENAME TO ${ident(tableName)};`))
|
||||
|
||||
for (const indexSql of keptIndexes) await db.run(sql.raw(`${indexSql};`))
|
||||
}
|
||||
|
||||
export async function up({ db, payload: _payload, req: _req }: MigrateUpArgs): Promise<void> {
|
||||
await addColumn(
|
||||
db,
|
||||
'pages',
|
||||
liveVideoColumn,
|
||||
'integer REFERENCES `media`(`id`) ON UPDATE no action ON DELETE set null',
|
||||
)
|
||||
await addColumn(
|
||||
db,
|
||||
'_pages_v',
|
||||
versionVideoColumn,
|
||||
'integer REFERENCES `media`(`id`) ON UPDATE no action ON DELETE set null',
|
||||
)
|
||||
|
||||
for (const [columnName, defaultValue] of textColumns) {
|
||||
await addColumn(db, 'pages', columnName, `text DEFAULT ${literal(defaultValue)}`)
|
||||
await addColumn(db, '_pages_v', `version_${columnName}`, `text DEFAULT ${literal(defaultValue)}`)
|
||||
}
|
||||
|
||||
await db.run(
|
||||
sql.raw(`CREATE INDEX IF NOT EXISTS ${ident(liveVideoIndex)} ON ${ident('pages')} (${ident(liveVideoColumn)});`),
|
||||
)
|
||||
await db.run(
|
||||
sql.raw(
|
||||
`CREATE INDEX IF NOT EXISTS ${ident(versionVideoIndex)} ON ${ident('_pages_v')} (${ident(versionVideoColumn)});`,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
export async function down({ db, payload: _payload, req: _req }: MigrateDownArgs): Promise<void> {
|
||||
const liveColumns = [liveVideoColumn, ...textColumns.map(([columnName]) => columnName)]
|
||||
const versionColumns = [versionVideoColumn, ...textColumns.map(([columnName]) => `version_${columnName}`)]
|
||||
|
||||
await db.run(sql.raw('PRAGMA foreign_keys=OFF;'))
|
||||
try {
|
||||
await rebuildTableWithoutColumns(db, 'pages', liveColumns)
|
||||
await rebuildTableWithoutColumns(db, '_pages_v', versionColumns)
|
||||
} finally {
|
||||
await db.run(sql.raw('PRAGMA foreign_keys=ON;'))
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,7 @@ import * as migration_20260713_213000_netzwerk_jury_selection from './20260713_2
|
||||
import * as migration_20260713_220000_preistraeger_detail_cleanup from './20260713_220000_preistraeger_detail_cleanup';
|
||||
import * as migration_20260731_143000_remove_press_record_fallbacks from './20260731_143000_remove_press_record_fallbacks';
|
||||
import * as migration_20260731_160000_focused_payload_cleanup from './20260731_160000_focused_payload_cleanup';
|
||||
import * as migration_20260731_170000_netzwerk_greeting_video from './20260731_170000_netzwerk_greeting_video';
|
||||
|
||||
export const migrations = [
|
||||
{
|
||||
@@ -114,4 +115,9 @@ export const migrations = [
|
||||
down: migration_20260731_160000_focused_payload_cleanup.down,
|
||||
name: '20260731_160000_focused_payload_cleanup',
|
||||
},
|
||||
{
|
||||
up: migration_20260731_170000_netzwerk_greeting_video.up,
|
||||
down: migration_20260731_170000_netzwerk_greeting_video.down,
|
||||
name: '20260731_170000_netzwerk_greeting_video',
|
||||
},
|
||||
];
|
||||
|
||||
@@ -1263,6 +1263,14 @@ export interface Page {
|
||||
greetingTitleLine2?: string | null;
|
||||
greetingQuote?: string | null;
|
||||
greetingAttribution?: string | null;
|
||||
/**
|
||||
* Choose the Ilse Aigner greeting video. If empty, the video action is hidden.
|
||||
*/
|
||||
greetingVideo?: (number | null) | Media;
|
||||
greetingVideoButtonLabel?: string | null;
|
||||
greetingVideoTitle?: string | null;
|
||||
greetingVideoDescription?: string | null;
|
||||
greetingVideoCloseLabel?: string | null;
|
||||
};
|
||||
/**
|
||||
* Editorial jury intro and stat strips.
|
||||
@@ -3438,6 +3446,11 @@ export interface PagesSelect<T extends boolean = true> {
|
||||
greetingTitleLine2?: T;
|
||||
greetingQuote?: T;
|
||||
greetingAttribution?: T;
|
||||
greetingVideo?: T;
|
||||
greetingVideoButtonLabel?: T;
|
||||
greetingVideoTitle?: T;
|
||||
greetingVideoDescription?: T;
|
||||
greetingVideoCloseLabel?: T;
|
||||
};
|
||||
juryIntro?:
|
||||
| T
|
||||
|
||||
@@ -136,6 +136,7 @@ async function main() {
|
||||
const { imageFilename: _heroFilename, ...heroContent } = netzwerkContent.hero
|
||||
const { imageFilename: _sponsoringFilename, ...sponsoringContent } = netzwerkContent.sponsoring
|
||||
const { items: _partnerItems, ...partnerSectionContent } = netzwerkContent.partners
|
||||
const existingGreetingVideo = page.netzwerk?.patronage?.greetingVideo || null
|
||||
const [patronPeople, juryMembers] = await Promise.all([
|
||||
itemsWithImageUploads(payload, netzwerkContent.patronage.people),
|
||||
juryMembersForPage(payload),
|
||||
@@ -155,6 +156,7 @@ async function main() {
|
||||
},
|
||||
patronage: {
|
||||
...netzwerkContent.patronage,
|
||||
greetingVideo: existingGreetingVideo,
|
||||
people: patronPeople,
|
||||
},
|
||||
jury: {
|
||||
|
||||
@@ -38,6 +38,10 @@ export const netzwerkContent = {
|
||||
greetingQuote:
|
||||
'„Der Bayerische Mittelstandspreis steht für das, was Bayern stark macht: Unternehmergeist, Verantwortung und Qualität. Mit großer Freude übernehme ich erneut die Schirmherrschaft für diesen bedeutenden Preis.“',
|
||||
greetingAttribution: '– Ilse Aigner MdL, Präsidentin des Bayerischen Landtags',
|
||||
greetingVideoButtonLabel: 'Film abspielen',
|
||||
greetingVideoTitle: 'Grußwort von Ilse Aigner MdL',
|
||||
greetingVideoDescription: 'Video-Grußwort der Schirmherrin des Bayerischen Mittelstandspreises.',
|
||||
greetingVideoCloseLabel: 'Schließen',
|
||||
},
|
||||
juryIntro: {
|
||||
eyebrow: 'Wer entscheidet?',
|
||||
|
||||
@@ -6,7 +6,6 @@ import MunichSkylineBg from '@/spa/components/ui/munich-skyline-bg';
|
||||
import { useIsMobile } from '@/spa/hooks/useIsMobile';
|
||||
import { useCmsCollection, useCmsRoute, type CmsRouteDoc } from '@/spa/cmsRoute';
|
||||
import { mediaAlt, mediaUrl } from '@/spa/cmsMediaField';
|
||||
import { homeContent } from '@/spa/homeContent';
|
||||
import { selectJuryDocs, sortJuryMembersQuoteFirst } from '@/spa/juryMembers';
|
||||
import { netzwerkContent } from '@/spa/netzwerkContent';
|
||||
import Image from '@/spa/components/ui/UnoptimizedImage'
|
||||
@@ -19,22 +18,11 @@ const GOLD = '#EFBF04';
|
||||
|
||||
type NetzwerkCms = Omit<Partial<typeof netzwerkContent>, 'hero' | 'jury' | 'sponsoring'> & {
|
||||
hero?: Partial<typeof netzwerkContent.hero> & { image?: unknown }
|
||||
patronage?: Partial<typeof netzwerkContent.patronage> & { greetingVideo?: unknown }
|
||||
jury?: Partial<Omit<typeof netzwerkContent.jury, 'members'>> & { members?: unknown[] | null }
|
||||
sponsoring?: Partial<typeof netzwerkContent.sponsoring> & { image?: unknown }
|
||||
}
|
||||
|
||||
type HomeVideoCms = {
|
||||
hero?: {
|
||||
videoLabel?: string | null
|
||||
}
|
||||
videoModal?: {
|
||||
video?: unknown
|
||||
title?: string | null
|
||||
description?: string | null
|
||||
closeLabel?: string | null
|
||||
}
|
||||
}
|
||||
|
||||
type PatronPerson = (typeof netzwerkContent.patronage.people)[number] & { imageUpload?: unknown }
|
||||
type JuryMember = (typeof netzwerkContent.jury.members)[number] & {
|
||||
active?: boolean | null
|
||||
@@ -468,11 +456,8 @@ const Netzwerk: React.FC = () => {
|
||||
const [showGreetingVideo, setShowGreetingVideo] = useState(false);
|
||||
const greetingVideoRef = useRef<HTMLVideoElement>(null);
|
||||
const cms = (useCmsRoute()?.doc?.netzwerk || {}) as NetzwerkCms;
|
||||
const homePage = useCmsCollection('pages').find((page) => page.spaPath === '/' || page.slug === 'startseite' || page.slug === 'home');
|
||||
const cmsPartners = useCmsCollection('partners') as unknown as Partner[];
|
||||
const cmsJuryMembers = useCmsCollection('jury-mitglieder');
|
||||
const home = (homePage?.home || {}) as HomeVideoCms;
|
||||
const homeVideoModal = home.videoModal || {};
|
||||
const hero = { ...netzwerkContent.hero, ...(cms.hero || {}) };
|
||||
const patronage = { ...netzwerkContent.patronage, ...(cms.patronage || {}) };
|
||||
const juryIntro = { ...netzwerkContent.juryIntro, ...(cms.juryIntro || {}) };
|
||||
@@ -499,10 +484,11 @@ const Netzwerk: React.FC = () => {
|
||||
const mobileStats = fallbackArray<StatItem>(juryIntro.stats, netzwerkContent.juryIntro.stats);
|
||||
const desktopStats = fallbackArray<StatItem>(juryIntro.desktopStats, netzwerkContent.juryIntro.desktopStats);
|
||||
const sponsorBenefits = fallbackArray<BenefitItem>(sponsoring.benefits, netzwerkContent.sponsoring.benefits);
|
||||
const greetingVideoSrc = mediaUrl(homeVideoModal.video, '');
|
||||
const greetingVideoTitle = fallbackText(homeVideoModal.title, homeContent.videoModal.title);
|
||||
const greetingVideoLabel = fallbackText(home.hero?.videoLabel, homeContent.hero.videoLabel);
|
||||
const greetingVideoCloseLabel = fallbackText(homeVideoModal.closeLabel, homeContent.videoModal.closeLabel);
|
||||
const greetingVideoSrc = mediaUrl(patronage.greetingVideo, '');
|
||||
const greetingVideoTitle = fallbackText(patronage.greetingVideoTitle, netzwerkContent.patronage.greetingVideoTitle);
|
||||
const greetingVideoDescription = fallbackText(patronage.greetingVideoDescription, netzwerkContent.patronage.greetingVideoDescription);
|
||||
const greetingVideoLabel = fallbackText(patronage.greetingVideoButtonLabel, netzwerkContent.patronage.greetingVideoButtonLabel);
|
||||
const greetingVideoCloseLabel = fallbackText(patronage.greetingVideoCloseLabel, netzwerkContent.patronage.greetingVideoCloseLabel);
|
||||
|
||||
useEffect(() => {
|
||||
if (!showGreetingVideo || !greetingVideoSrc) return;
|
||||
@@ -850,6 +836,10 @@ const Netzwerk: React.FC = () => {
|
||||
{showGreetingVideo && greetingVideoSrc && (
|
||||
<div
|
||||
onClick={(e) => { if (e.target === e.currentTarget) setShowGreetingVideo(false); }}
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="netzwerk-greeting-video-title"
|
||||
aria-describedby="netzwerk-greeting-video-description"
|
||||
style={{
|
||||
position: 'fixed',
|
||||
inset: 0,
|
||||
@@ -861,6 +851,8 @@ const Netzwerk: React.FC = () => {
|
||||
padding: isMobile ? 16 : 24,
|
||||
}}
|
||||
>
|
||||
<h2 id="netzwerk-greeting-video-title" className="sr-only">{greetingVideoTitle}</h2>
|
||||
<p id="netzwerk-greeting-video-description" className="sr-only">{greetingVideoDescription}</p>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowGreetingVideo(false)}
|
||||
|
||||
120
tests/int/netzwerk-video.int.spec.tsx
Normal file
120
tests/int/netzwerk-video.int.spec.tsx
Normal file
@@ -0,0 +1,120 @@
|
||||
import { cleanup, fireEvent, render, screen } from '@testing-library/react'
|
||||
import { afterEach, beforeAll, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { CmsRouteProvider, type CmsRouteData, type CmsRouteDoc } from '@/spa/cmsRoute'
|
||||
import { netzwerkContent } from '@/spa/netzwerkContent'
|
||||
import Netzwerk from '@/spa/pages/Netzwerk'
|
||||
|
||||
vi.mock('next/navigation', () => ({
|
||||
usePathname: () => '/netzwerk',
|
||||
useRouter: () => ({ push: vi.fn(), replace: vi.fn() }),
|
||||
}))
|
||||
|
||||
const homePage = (videoUrl: string): CmsRouteDoc => ({
|
||||
id: `home-${videoUrl}`,
|
||||
slug: 'startseite',
|
||||
spaPath: '/',
|
||||
home: {
|
||||
hero: { videoLabel: 'Homepage film' },
|
||||
videoModal: {
|
||||
video: { url: videoUrl },
|
||||
title: 'Homepage video',
|
||||
closeLabel: 'Homepage video close',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const networkRoute = (video?: { url: string; label: string; title: string }): CmsRouteData => ({
|
||||
collection: 'pages',
|
||||
doc: {
|
||||
id: 'network-page',
|
||||
slug: 'netzwerk',
|
||||
spaPath: '/netzwerk',
|
||||
netzwerk: {
|
||||
patronage: {
|
||||
...(video
|
||||
? {
|
||||
greetingVideo: { url: video.url },
|
||||
greetingVideoButtonLabel: video.label,
|
||||
greetingVideoTitle: video.title,
|
||||
greetingVideoDescription: `${video.title} description`,
|
||||
greetingVideoCloseLabel: `${video.title} close`,
|
||||
}
|
||||
: {}),
|
||||
},
|
||||
},
|
||||
},
|
||||
lists: {
|
||||
pages: [homePage('/api/media/file/homepage.mp4')],
|
||||
partners: [],
|
||||
'jury-mitglieder': [],
|
||||
},
|
||||
})
|
||||
|
||||
describe('Netzwerk greeting video', () => {
|
||||
beforeAll(() => {
|
||||
Object.defineProperty(window, 'matchMedia', {
|
||||
configurable: true,
|
||||
value: vi.fn().mockImplementation((query: string) => ({
|
||||
addEventListener: vi.fn(),
|
||||
matches: false,
|
||||
media: query,
|
||||
removeEventListener: vi.fn(),
|
||||
})),
|
||||
})
|
||||
vi.spyOn(HTMLMediaElement.prototype, 'play').mockResolvedValue(undefined)
|
||||
})
|
||||
|
||||
afterEach(() => cleanup())
|
||||
|
||||
it('does not fall back to the homepage video when Netzwerk media is empty', () => {
|
||||
render(
|
||||
<CmsRouteProvider value={networkRoute()}>
|
||||
<Netzwerk />
|
||||
</CmsRouteProvider>,
|
||||
)
|
||||
|
||||
expect(screen.queryByRole('button', { name: netzwerkContent.patronage.greetingVideoButtonLabel })).toBeNull()
|
||||
expect(screen.queryByLabelText('Homepage video')).toBeNull()
|
||||
})
|
||||
|
||||
it('resolves the page-owned Netzwerk media independently of homepage changes', () => {
|
||||
const firstNetworkVideo = {
|
||||
url: '/api/media/file/network-first.mp4',
|
||||
label: 'Play Netzwerk greeting',
|
||||
title: 'Netzwerk greeting one',
|
||||
}
|
||||
const secondNetworkVideo = {
|
||||
url: '/api/media/file/network-second.mp4',
|
||||
label: 'Play second Netzwerk greeting',
|
||||
title: 'Netzwerk greeting two',
|
||||
}
|
||||
const { rerender } = render(
|
||||
<CmsRouteProvider value={networkRoute(firstNetworkVideo)}>
|
||||
<Netzwerk />
|
||||
</CmsRouteProvider>,
|
||||
)
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: firstNetworkVideo.label }))
|
||||
expect(screen.getByRole('dialog').querySelector('video')?.getAttribute('src')).toBe(firstNetworkVideo.url)
|
||||
|
||||
const homeChanged = networkRoute(firstNetworkVideo)
|
||||
homeChanged.lists = {
|
||||
...homeChanged.lists,
|
||||
pages: [homePage('/api/media/file/homepage-changed.mp4')],
|
||||
}
|
||||
rerender(
|
||||
<CmsRouteProvider value={homeChanged}>
|
||||
<Netzwerk />
|
||||
</CmsRouteProvider>,
|
||||
)
|
||||
expect(screen.getByRole('dialog').querySelector('video')?.getAttribute('src')).toBe(firstNetworkVideo.url)
|
||||
|
||||
rerender(
|
||||
<CmsRouteProvider value={networkRoute(secondNetworkVideo)}>
|
||||
<Netzwerk />
|
||||
</CmsRouteProvider>,
|
||||
)
|
||||
expect(screen.getByRole('dialog').querySelector('video')?.getAttribute('src')).toBe(secondNetworkVideo.url)
|
||||
})
|
||||
})
|
||||
@@ -99,6 +99,11 @@ describe('Payload field inventory', () => {
|
||||
'pressIndex.events.statusLabels',
|
||||
'pressIndex.downloads.contactName',
|
||||
'netzwerk.jury.members',
|
||||
'netzwerk.patronage.greetingVideo',
|
||||
'netzwerk.patronage.greetingVideoButtonLabel',
|
||||
'netzwerk.patronage.greetingVideoTitle',
|
||||
'netzwerk.patronage.greetingVideoDescription',
|
||||
'netzwerk.patronage.greetingVideoCloseLabel',
|
||||
'netzwerk.partners.tiers',
|
||||
'netzwerk.partners.modal.websiteLabel',
|
||||
'netzwerk.sponsoringForm.steps',
|
||||
@@ -130,4 +135,19 @@ describe('Payload field inventory', () => {
|
||||
maxRows: 2,
|
||||
})
|
||||
})
|
||||
|
||||
it('limits the Netzwerk greeting upload to video media', () => {
|
||||
const field = findField(netzwerkFields, 'netzwerk.patronage.greetingVideo')
|
||||
|
||||
expect(field).toMatchObject({
|
||||
type: 'upload',
|
||||
relationTo: 'media',
|
||||
filterOptions: {
|
||||
mimeType: {
|
||||
contains: 'video',
|
||||
},
|
||||
},
|
||||
})
|
||||
expect(field).not.toHaveProperty('defaultValue')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -7,6 +7,6 @@ export default defineConfig({
|
||||
test: {
|
||||
environment: 'jsdom',
|
||||
setupFiles: ['./vitest.setup.ts'],
|
||||
include: ['tests/int/**/*.int.spec.ts'],
|
||||
include: ['tests/int/**/*.int.spec.{ts,tsx}'],
|
||||
},
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user