feat(cms): add press contact image
This commit is contained in:
@@ -29,7 +29,7 @@ This inventory records the frontend or platform owner for every retained `Pages`
|
||||
| `formUpload.*` | `src/spa/pages/FormularUpload.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. |
|
||||
| `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.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. |
|
||||
|
||||
@@ -2,11 +2,20 @@ import type { Field } from 'payload'
|
||||
|
||||
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,
|
||||
type: 'upload',
|
||||
relationTo: 'media',
|
||||
label,
|
||||
...(imageOnly
|
||||
? {
|
||||
filterOptions: {
|
||||
mimeType: {
|
||||
contains: 'image',
|
||||
},
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
admin: description ? { description } : undefined,
|
||||
})
|
||||
|
||||
@@ -108,6 +117,13 @@ export const pressIndexFields: Field[] = [
|
||||
text('contactEyebrow', 'Contact eyebrow', pressIndexContent.downloads.contactEyebrow),
|
||||
text('contactName', 'Contact name', pressIndexContent.downloads.contactName),
|
||||
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),
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
181
src/migrations/20260731_180000_press_contact_image.ts
Normal file
181
src/migrations/20260731_180000_press_contact_image.ts
Normal 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;'))
|
||||
}
|
||||
}
|
||||
@@ -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_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_180000_press_contact_image from './20260731_180000_press_contact_image';
|
||||
|
||||
export const migrations = [
|
||||
{
|
||||
@@ -120,4 +121,9 @@ export const migrations = [
|
||||
down: migration_20260731_170000_netzwerk_greeting_video.down,
|
||||
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',
|
||||
},
|
||||
];
|
||||
|
||||
@@ -1139,6 +1139,11 @@ export interface Page {
|
||||
contactEyebrow?: string | null;
|
||||
contactName?: 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;
|
||||
contactName?: T;
|
||||
contactLines?: T;
|
||||
contactImage?: T;
|
||||
contactImageAlt?: T;
|
||||
};
|
||||
};
|
||||
formUpload?:
|
||||
|
||||
@@ -31,6 +31,11 @@ async function main() {
|
||||
const { imageFilename: _heroFilename, ...heroContent } = pressIndexContent.hero
|
||||
const { collectionFallbackImageFilename: _eventFallbackFilename, ...eventsContent } = pressIndexContent.events
|
||||
const { collectionFallbackImageFilename: _newsFallbackFilename, ...newsContent } = pressIndexContent.news
|
||||
const currentContactImage = page.pressIndex?.downloads?.contactImage
|
||||
const currentContactImageID =
|
||||
currentContactImage && typeof currentContactImage === 'object'
|
||||
? currentContactImage.id
|
||||
: currentContactImage
|
||||
await payload.update({
|
||||
collection: 'pages',
|
||||
id: page.id,
|
||||
@@ -51,7 +56,12 @@ async function main() {
|
||||
...newsContent,
|
||||
collectionFallbackImage: newsFallbackImage,
|
||||
},
|
||||
downloads: pressIndexContent.downloads,
|
||||
downloads: {
|
||||
...pressIndexContent.downloads,
|
||||
contactImage: currentContactImageID ?? null,
|
||||
contactImageAlt:
|
||||
page.pressIndex?.downloads?.contactImageAlt ?? pressIndexContent.downloads.contactImageAlt,
|
||||
},
|
||||
},
|
||||
} as never,
|
||||
})
|
||||
|
||||
@@ -23,7 +23,7 @@ type PressIndexCms = Partial<typeof pressIndexContent> & {
|
||||
hero?: Partial<typeof pressIndexContent.hero> & { image?: unknown }
|
||||
events?: Partial<typeof pressIndexContent.events> & { 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;
|
||||
@@ -146,6 +146,10 @@ const Press: React.FC = () => {
|
||||
const newsSection = React.useMemo(() => ({ ...pressIndexContent.news, ...(cms.news || {}) }), [cms.news]);
|
||||
const downloads = React.useMemo(() => ({ ...pressIndexContent.downloads, ...(cms.downloads || {}) }), [cms.downloads]);
|
||||
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 cmsPosts = useCmsCollection('posts');
|
||||
const eventStatusLabels = React.useMemo(() => statusLabelMap(eventsSection.statusLabels), [eventsSection.statusLabels]);
|
||||
@@ -385,13 +389,21 @@ const Press: React.FC = () => {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* ── 4. PRESSE-MATERIAL & AKKREDITIERUNG ─────────────────────────── */}
|
||||
{/* ── 4. PRESSE-MATERIAL & KONTAKT ────────────────────────────────── */}
|
||||
<section id="downloads" style={{ background: '#fff', overflow: 'hidden', position: 'relative', isolation: 'isolate' }}>
|
||||
<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
|
||||
data-testid="press-contact-copy"
|
||||
style={{
|
||||
background: '#fff',
|
||||
padding: isMobile ? '48px 24px' : '88px 80px',
|
||||
@@ -481,6 +493,25 @@ const Press: React.FC = () => {
|
||||
</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>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
@@ -39,5 +39,6 @@ export const pressIndexContent = {
|
||||
contactEyebrow: 'Pressekontakt',
|
||||
contactName: 'Tanja Meier',
|
||||
contactLines: 'presse@bmp-bayern.de\n+49 89 123 456 99',
|
||||
contactImageAlt: '',
|
||||
},
|
||||
}
|
||||
|
||||
@@ -98,6 +98,8 @@ describe('Payload field inventory', () => {
|
||||
'about.testimonials.items',
|
||||
'pressIndex.events.statusLabels',
|
||||
'pressIndex.downloads.contactName',
|
||||
'pressIndex.downloads.contactImage',
|
||||
'pressIndex.downloads.contactImageAlt',
|
||||
'netzwerk.jury.members',
|
||||
'netzwerk.patronage.greetingVideo',
|
||||
'netzwerk.patronage.greetingVideoButtonLabel',
|
||||
@@ -150,4 +152,19 @@ describe('Payload field inventory', () => {
|
||||
})
|
||||
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')
|
||||
})
|
||||
})
|
||||
|
||||
123
tests/int/press-contact-image.int.spec.tsx
Normal file
123
tests/int/press-contact-image.int.spec.tsx
Normal 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()
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user