feat(cms): add press contact image

This commit is contained in:
syntaxbullet
2026-07-31 15:21:21 +02:00
parent e3c47c4500
commit c2dc42f243
10 changed files with 399 additions and 7 deletions

View File

@@ -29,7 +29,7 @@ This inventory records the frontend or platform owner for every retained `Pages`
| `formUpload.*` | `src/spa/pages/FormularUpload.tsx`. | | `formUpload.*` | `src/spa/pages/FormularUpload.tsx`. |
| `mitgliedWerden.*` | `src/spa/pages/MitgliedWerden.tsx` and `src/spa/components/membership/MembershipWizard.tsx`. | | `mitgliedWerden.*` | `src/spa/pages/MitgliedWerden.tsx` and `src/spa/components/membership/MembershipWizard.tsx`. |
| `preistraegerIndex.*` | `src/spa/pages/Preistraeger.tsx`; winner records remain collection-owned. | | `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. | | `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. `pressIndex.downloads.contactImage` and `contactImageAlt` own the optional Presse contact visual; an empty relationship renders the same copy in a compact copy-only layout without a static image fallback. |
| `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.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.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.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. |

View File

@@ -2,11 +2,20 @@ import type { Field } from 'payload'
import { pressIndexContent } from '@/spa/pressIndexContent' import { pressIndexContent } from '@/spa/pressIndexContent'
const uploadField = (name: string, label: string, description?: string): Field => ({ const uploadField = (name: string, label: string, description?: string, imageOnly = false): Field => ({
name, name,
type: 'upload', type: 'upload',
relationTo: 'media', relationTo: 'media',
label, label,
...(imageOnly
? {
filterOptions: {
mimeType: {
contains: 'image',
},
},
}
: {}),
admin: description ? { description } : undefined, admin: description ? { description } : undefined,
}) })
@@ -108,6 +117,13 @@ export const pressIndexFields: Field[] = [
text('contactEyebrow', 'Contact eyebrow', pressIndexContent.downloads.contactEyebrow), text('contactEyebrow', 'Contact eyebrow', pressIndexContent.downloads.contactEyebrow),
text('contactName', 'Contact name', pressIndexContent.downloads.contactName), text('contactName', 'Contact name', pressIndexContent.downloads.contactName),
textarea('contactLines', 'Contact lines', pressIndexContent.downloads.contactLines), textarea('contactLines', 'Contact lines', pressIndexContent.downloads.contactLines),
uploadField(
'contactImage',
'Contact image',
'Optional image shown beside the press contact. Clear the selection to use the copy-only layout.',
true,
),
text('contactImageAlt', 'Contact image alt text', pressIndexContent.downloads.contactImageAlt),
], ],
}, },
], ],

View File

@@ -0,0 +1,181 @@
import { type MigrateDownArgs, type MigrateUpArgs, sql } from '@payloadcms/db-sqlite'
type MigrationDB = MigrateUpArgs['db']
const ident = (value: string) => `\`${value.replace(/`/g, '``')}\``
const literal = (value: string) => `'${value.replace(/'/g, "''")}'`
const liveImageColumn = 'press_index_downloads_contact_image_id'
const liveAltColumn = 'press_index_downloads_contact_image_alt'
const versionImageColumn = `version_${liveImageColumn}`
const versionAltColumn = `version_${liveAltColumn}`
const liveImageIndex = 'pages_press_index_downloads_press_index_downloads_contac_idx'
const versionImageIndex = '_pages_v_version_press_index_downloads_version_press_ind_idx'
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 = `__task6_${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',
liveImageColumn,
'integer REFERENCES `media`(`id`) ON UPDATE no action ON DELETE set null',
)
await addColumn(db, 'pages', liveAltColumn, "text DEFAULT ''")
await addColumn(
db,
'_pages_v',
versionImageColumn,
'integer REFERENCES `media`(`id`) ON UPDATE no action ON DELETE set null',
)
await addColumn(db, '_pages_v', versionAltColumn, "text DEFAULT ''")
await db.run(
sql.raw(
`CREATE INDEX IF NOT EXISTS ${ident(liveImageIndex)} ON ${ident('pages')} (${ident(liveImageColumn)});`,
),
)
await db.run(
sql.raw(
`CREATE INDEX IF NOT EXISTS ${ident(versionImageIndex)} ON ${ident('_pages_v')} (${ident(versionImageColumn)});`,
),
)
}
export async function down({ db, payload: _payload, req: _req }: MigrateDownArgs): Promise<void> {
await db.run(sql.raw('PRAGMA foreign_keys=OFF;'))
try {
await rebuildTableWithoutColumns(db, 'pages', [liveImageColumn, liveAltColumn])
await rebuildTableWithoutColumns(db, '_pages_v', [versionImageColumn, versionAltColumn])
} finally {
await db.run(sql.raw('PRAGMA foreign_keys=ON;'))
}
}

View File

@@ -18,6 +18,7 @@ import * as migration_20260713_220000_preistraeger_detail_cleanup from './202607
import * as migration_20260731_143000_remove_press_record_fallbacks from './20260731_143000_remove_press_record_fallbacks'; 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_160000_focused_payload_cleanup from './20260731_160000_focused_payload_cleanup';
import * as migration_20260731_170000_netzwerk_greeting_video from './20260731_170000_netzwerk_greeting_video'; import * as migration_20260731_170000_netzwerk_greeting_video from './20260731_170000_netzwerk_greeting_video';
import * as migration_20260731_180000_press_contact_image from './20260731_180000_press_contact_image';
export const migrations = [ export const migrations = [
{ {
@@ -120,4 +121,9 @@ export const migrations = [
down: migration_20260731_170000_netzwerk_greeting_video.down, down: migration_20260731_170000_netzwerk_greeting_video.down,
name: '20260731_170000_netzwerk_greeting_video', name: '20260731_170000_netzwerk_greeting_video',
}, },
{
up: migration_20260731_180000_press_contact_image.up,
down: migration_20260731_180000_press_contact_image.down,
name: '20260731_180000_press_contact_image',
},
]; ];

View File

@@ -1139,6 +1139,11 @@ export interface Page {
contactEyebrow?: string | null; contactEyebrow?: string | null;
contactName?: string | null; contactName?: string | null;
contactLines?: string | null; contactLines?: string | null;
/**
* Optional image shown beside the press contact. Clear the selection to use the copy-only layout.
*/
contactImage?: (number | null) | Media;
contactImageAlt?: string | null;
}; };
}; };
/** /**
@@ -3335,6 +3340,8 @@ export interface PagesSelect<T extends boolean = true> {
contactEyebrow?: T; contactEyebrow?: T;
contactName?: T; contactName?: T;
contactLines?: T; contactLines?: T;
contactImage?: T;
contactImageAlt?: T;
}; };
}; };
formUpload?: formUpload?:

View File

@@ -31,6 +31,11 @@ async function main() {
const { imageFilename: _heroFilename, ...heroContent } = pressIndexContent.hero const { imageFilename: _heroFilename, ...heroContent } = pressIndexContent.hero
const { collectionFallbackImageFilename: _eventFallbackFilename, ...eventsContent } = pressIndexContent.events const { collectionFallbackImageFilename: _eventFallbackFilename, ...eventsContent } = pressIndexContent.events
const { collectionFallbackImageFilename: _newsFallbackFilename, ...newsContent } = pressIndexContent.news const { collectionFallbackImageFilename: _newsFallbackFilename, ...newsContent } = pressIndexContent.news
const currentContactImage = page.pressIndex?.downloads?.contactImage
const currentContactImageID =
currentContactImage && typeof currentContactImage === 'object'
? currentContactImage.id
: currentContactImage
await payload.update({ await payload.update({
collection: 'pages', collection: 'pages',
id: page.id, id: page.id,
@@ -51,7 +56,12 @@ async function main() {
...newsContent, ...newsContent,
collectionFallbackImage: newsFallbackImage, collectionFallbackImage: newsFallbackImage,
}, },
downloads: pressIndexContent.downloads, downloads: {
...pressIndexContent.downloads,
contactImage: currentContactImageID ?? null,
contactImageAlt:
page.pressIndex?.downloads?.contactImageAlt ?? pressIndexContent.downloads.contactImageAlt,
},
}, },
} as never, } as never,
}) })

View File

@@ -23,7 +23,7 @@ type PressIndexCms = Partial<typeof pressIndexContent> & {
hero?: Partial<typeof pressIndexContent.hero> & { image?: unknown } hero?: Partial<typeof pressIndexContent.hero> & { image?: unknown }
events?: Partial<typeof pressIndexContent.events> & { collectionFallbackImage?: unknown } events?: Partial<typeof pressIndexContent.events> & { collectionFallbackImage?: unknown }
news?: Partial<typeof pressIndexContent.news> & { collectionFallbackImage?: unknown } news?: Partial<typeof pressIndexContent.news> & { collectionFallbackImage?: unknown }
downloads?: Partial<typeof pressIndexContent.downloads> downloads?: Partial<typeof pressIndexContent.downloads> & { contactImage?: unknown }
} }
const fallbackText = (value: unknown, fallback: string) => typeof value === 'string' && value.length > 0 ? value : fallback; const fallbackText = (value: unknown, fallback: string) => typeof value === 'string' && value.length > 0 ? value : fallback;
@@ -146,6 +146,10 @@ const Press: React.FC = () => {
const newsSection = React.useMemo(() => ({ ...pressIndexContent.news, ...(cms.news || {}) }), [cms.news]); const newsSection = React.useMemo(() => ({ ...pressIndexContent.news, ...(cms.news || {}) }), [cms.news]);
const downloads = React.useMemo(() => ({ ...pressIndexContent.downloads, ...(cms.downloads || {}) }), [cms.downloads]); const downloads = React.useMemo(() => ({ ...pressIndexContent.downloads, ...(cms.downloads || {}) }), [cms.downloads]);
const contactLines = fallbackText(downloads.contactLines, pressIndexContent.downloads.contactLines).split('\n'); const contactLines = fallbackText(downloads.contactLines, pressIndexContent.downloads.contactLines).split('\n');
const contactImageUrl = mediaUrl(downloads.contactImage, '');
const contactImageAlt = typeof downloads.contactImageAlt === 'string'
? downloads.contactImageAlt
: mediaAlt(downloads.contactImage, '');
const cmsEvents = useCmsCollection('events'); const cmsEvents = useCmsCollection('events');
const cmsPosts = useCmsCollection('posts'); const cmsPosts = useCmsCollection('posts');
const eventStatusLabels = React.useMemo(() => statusLabelMap(eventsSection.statusLabels), [eventsSection.statusLabels]); const eventStatusLabels = React.useMemo(() => statusLabelMap(eventsSection.statusLabels), [eventsSection.statusLabels]);
@@ -385,13 +389,21 @@ const Press: React.FC = () => {
</div> </div>
</section> </section>
{/* ── 4. PRESSE-MATERIAL & AKKREDITIERUNG ─────────────────────────── */} {/* ── 4. PRESSE-MATERIAL & KONTAKT ────────────────────────────────── */}
<section id="downloads" style={{ background: '#fff', overflow: 'hidden', position: 'relative', isolation: 'isolate' }}> <section id="downloads" style={{ background: '#fff', overflow: 'hidden', position: 'relative', isolation: 'isolate' }}>
<MunichSkylineBg /> <MunichSkylineBg />
<div style={{ display: 'grid', gridTemplateColumns: '1fr', minHeight: isMobile ? 'auto' : 600 }}> <div
data-testid="press-contact-layout"
data-layout={contactImageUrl ? (isMobile ? 'stacked' : 'two-column') : 'copy-only'}
style={{
display: 'grid',
gridTemplateColumns: isMobile || !contactImageUrl ? '1fr' : 'minmax(0, 1fr) minmax(320px, 1fr)',
}}
>
{/* Left col */} {/* Copy comes first in the DOM and visually, including in the mobile stack. */}
<div <div
data-testid="press-contact-copy"
style={{ style={{
background: '#fff', background: '#fff',
padding: isMobile ? '48px 24px' : '88px 80px', padding: isMobile ? '48px 24px' : '88px 80px',
@@ -481,6 +493,25 @@ const Press: React.FC = () => {
</div> </div>
</div> </div>
</div> </div>
{contactImageUrl && (
<div
data-testid="press-contact-image"
style={{
minHeight: isMobile ? 320 : 560,
position: 'relative',
overflow: 'hidden',
background: NAVY,
}}
>
<Image
unoptimized
src={contactImageUrl}
alt={contactImageAlt}
style={{ width: '100%', height: '100%', position: 'absolute', inset: 0, objectFit: 'cover', display: 'block' }}
/>
</div>
)}
</div> </div>
</section> </section>
</div> </div>

View File

@@ -39,5 +39,6 @@ export const pressIndexContent = {
contactEyebrow: 'Pressekontakt', contactEyebrow: 'Pressekontakt',
contactName: 'Tanja Meier', contactName: 'Tanja Meier',
contactLines: 'presse@bmp-bayern.de\n+49 89 123 456 99', contactLines: 'presse@bmp-bayern.de\n+49 89 123 456 99',
contactImageAlt: '',
}, },
} }

View File

@@ -98,6 +98,8 @@ describe('Payload field inventory', () => {
'about.testimonials.items', 'about.testimonials.items',
'pressIndex.events.statusLabels', 'pressIndex.events.statusLabels',
'pressIndex.downloads.contactName', 'pressIndex.downloads.contactName',
'pressIndex.downloads.contactImage',
'pressIndex.downloads.contactImageAlt',
'netzwerk.jury.members', 'netzwerk.jury.members',
'netzwerk.patronage.greetingVideo', 'netzwerk.patronage.greetingVideo',
'netzwerk.patronage.greetingVideoButtonLabel', 'netzwerk.patronage.greetingVideoButtonLabel',
@@ -150,4 +152,19 @@ describe('Payload field inventory', () => {
}) })
expect(field).not.toHaveProperty('defaultValue') expect(field).not.toHaveProperty('defaultValue')
}) })
it('limits the press contact upload to image media', () => {
const field = findField(pressIndexFields, 'pressIndex.downloads.contactImage')
expect(field).toMatchObject({
type: 'upload',
relationTo: 'media',
filterOptions: {
mimeType: {
contains: 'image',
},
},
})
expect(field).not.toHaveProperty('defaultValue')
})
}) })

View File

@@ -0,0 +1,123 @@
import { cleanup, render, screen, within } from '@testing-library/react'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { CmsRouteProvider, type CmsRouteData } from '@/spa/cmsRoute'
import { pressIndexContent } from '@/spa/pressIndexContent'
import Press from '@/spa/pages/Press'
const viewport = vi.hoisted(() => ({ mobile: false }))
vi.mock('@/spa/hooks/useIsMobile', () => ({
useIsMobile: () => viewport.mobile,
}))
vi.mock('next/navigation', () => ({
usePathname: () => '/presse',
useRouter: () => ({ push: vi.fn(), replace: vi.fn() }),
}))
const pressRoute = (contactImage?: unknown, contactImageAlt?: string): CmsRouteData => ({
collection: 'pages',
doc: {
id: 'press-page',
slug: 'presse',
spaPath: '/presse',
pressIndex: {
downloads: {
contactImage,
contactImageAlt,
},
},
},
lists: {
events: [],
posts: [],
},
})
describe('Presse contact image', () => {
beforeEach(() => {
viewport.mobile = false
})
afterEach(() => cleanup())
it('renders selected media and page-owned alt text in the desktop right column', () => {
render(
<CmsRouteProvider
value={pressRoute(
{ url: '/api/media/file/press-contact.jpg', alt: 'Media default alt' },
'Press team portrait',
)}
>
<Press />
</CmsRouteProvider>,
)
const layout = screen.getByTestId('press-contact-layout')
const imageColumn = screen.getByTestId('press-contact-image')
const image = within(imageColumn).getByRole('img', { name: 'Press team portrait' })
expect(layout.getAttribute('data-layout')).toBe('two-column')
expect(layout.style.gridTemplateColumns).toBe('minmax(0, 1fr) minmax(320px, 1fr)')
expect(image.getAttribute('src')).toBe('/api/media/file/press-contact.jpg')
expect(
screen.getByTestId('press-contact-copy').compareDocumentPosition(imageColumn) &
Node.DOCUMENT_POSITION_FOLLOWING,
).toBeTruthy()
})
it('clears only the visual and keeps the contact copy in a compact fallback-free layout', () => {
const { rerender } = render(
<CmsRouteProvider
value={pressRoute({ url: '/api/media/file/press-contact.jpg' }, 'Press contact')}
>
<Press />
</CmsRouteProvider>,
)
expect(screen.getByTestId('press-contact-image')).toBeTruthy()
rerender(
<CmsRouteProvider value={pressRoute(undefined, '')}>
<Press />
</CmsRouteProvider>,
)
const layout = screen.getByTestId('press-contact-layout')
expect(screen.queryByTestId('press-contact-image')).toBeNull()
expect(layout.getAttribute('data-layout')).toBe('copy-only')
expect(layout.style.gridTemplateColumns).toBe('1fr')
expect(
within(screen.getByTestId('press-contact-copy')).getByText(
pressIndexContent.downloads.contactName,
),
).toBeTruthy()
expect(within(layout).queryByRole('img')).toBeNull()
})
it('stacks copy before the selected image on mobile', () => {
viewport.mobile = true
render(
<CmsRouteProvider
value={pressRoute({ filename: 'press-contact-mobile.jpg' }, 'Mobile press contact')}
>
<Press />
</CmsRouteProvider>,
)
const layout = screen.getByTestId('press-contact-layout')
const copy = screen.getByTestId('press-contact-copy')
const imageColumn = screen.getByTestId('press-contact-image')
expect(layout.getAttribute('data-layout')).toBe('stacked')
expect(layout.style.gridTemplateColumns).toBe('1fr')
expect(
within(imageColumn).getByRole('img', { name: 'Mobile press contact' }).getAttribute('src'),
).toBe('/api/media/file/press-contact-mobile.jpg')
expect(
copy.compareDocumentPosition(imageColumn) & Node.DOCUMENT_POSITION_FOLLOWING,
).toBeTruthy()
})
})