fix(about): align facts and caption
This commit is contained in:
@@ -100,7 +100,7 @@ export const aboutFields: Field[] = [
|
||||
defaultValue: aboutContent.prize.facts,
|
||||
fields: statFields,
|
||||
},
|
||||
text('imageLabel', 'Desktop image label', aboutContent.prize.imageLabel),
|
||||
text('imageLabel', 'Image caption', aboutContent.prize.imageLabel),
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -160,10 +160,10 @@ const About: React.FC = () => {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="was-ist" style={{ position: 'relative', overflow: 'hidden', isolation: 'isolate' }}>
|
||||
<section id="was-ist" data-testid="prize-section" style={{ position: 'relative', overflow: 'hidden', isolation: 'isolate' }}>
|
||||
<MunichSkylineBg />
|
||||
<div style={{ display: 'grid', gridTemplateColumns: isMobile ? '1fr' : '55% 45%', minHeight: isMobile ? 'auto' : 580 }}>
|
||||
<div style={{ background: WHITE, padding: isMobile ? '32px 24px 44px' : '88px 80px', display: 'flex', flexDirection: 'column', justifyContent: 'center', order: isMobile ? 2 : 0 }}>
|
||||
<div data-testid="prize-layout" data-layout={isRelevanceStacked ? 'stacked' : 'split'} style={{ display: 'grid', gridTemplateColumns: isRelevanceStacked ? '1fr' : '55% 45%', minHeight: isMobile ? 'auto' : 580 }}>
|
||||
<div data-testid="prize-copy-panel" style={{ background: WHITE, padding: isMobile ? '32px 24px 44px' : isRelevanceStacked ? '56px 48px' : '88px clamp(48px, 5.55vw, 80px)', display: 'flex', flexDirection: 'column', justifyContent: 'center', order: isRelevanceStacked ? 2 : 0 }}>
|
||||
{!isMobile && (
|
||||
<>
|
||||
<span style={{ fontFamily: FF, fontSize: 10, color: 'var(--text-eyebrow-light)', textTransform: 'uppercase', letterSpacing: '0.32em', fontWeight: 700, display: 'block', marginBottom: 20 }}>{fallbackText(prize.eyebrow, aboutContent.prize.eyebrow)}</span>
|
||||
@@ -178,41 +178,41 @@ const About: React.FC = () => {
|
||||
{text}
|
||||
</p>
|
||||
))}
|
||||
<div style={{ display: 'flex', order: isMobile ? -1 : 0, borderTop: isMobile ? 'none' : '1px solid #D0D5DD', borderBottom: isMobile ? '1px solid #D0D5DD' : 'none', marginBottom: isMobile ? 26 : 0 }}>
|
||||
<div data-testid="prize-facts" style={{ display: 'grid', gridTemplateColumns: 'repeat(3, minmax(0, 1fr))', order: isMobile ? -1 : 0, borderTop: isMobile ? 'none' : '1px solid #D0D5DD', borderBottom: isMobile ? '1px solid #D0D5DD' : 'none', marginBottom: isMobile ? 26 : 0 }}>
|
||||
{fallbackArray(prize.facts, aboutContent.prize.facts).map((f, i, arr) => (
|
||||
<div key={`${f.value}-${i}`} style={{ flex: 1, padding: '20px 0', borderRight: i < arr.length - 1 ? '1px solid #D0D5DD' : 'none', paddingLeft: 0, paddingRight: i < arr.length - 1 ? 20 : 0 }}>
|
||||
<div style={{ fontFamily: FF, fontSize: 'clamp(1.4rem, 2vw, 1.8rem)', fontWeight: 900, color: '#101828', letterSpacing: '-0.03em', lineHeight: 1 }}>{fallbackText(f.value, '')}</div>
|
||||
<div style={{ fontFamily: FF, fontSize: 12, fontWeight: 700, color: 'var(--text-brand-on-light)', textTransform: 'uppercase', letterSpacing: '0.18em', marginTop: 6 }}>{fallbackText(f.label, '')}</div>
|
||||
<div key={`${f.value}-${i}`} data-testid="prize-fact" style={{ minWidth: 0, padding: isMobile ? '20px 8px' : '20px 16px', borderRight: i < arr.length - 1 ? '1px solid #D0D5DD' : 'none', display: 'flex', flexDirection: 'column', alignItems: 'center', textAlign: 'center' }}>
|
||||
<div data-testid="prize-fact-value" style={{ fontFamily: FF, fontSize: 'clamp(1.4rem, 2vw, 1.8rem)', fontWeight: 900, color: '#101828', letterSpacing: '-0.03em', lineHeight: 1 }}>{fallbackText(f.value, '')}</div>
|
||||
<div data-testid="prize-fact-label" style={{ maxWidth: '100%', fontFamily: FF, fontSize: isMobile ? 10 : 11, fontWeight: 700, color: 'var(--text-brand-on-light)', textTransform: 'uppercase', letterSpacing: isMobile ? '0.11em' : '0.14em', lineHeight: 1.35, marginTop: 8, overflowWrap: 'anywhere', hyphens: 'auto' }}>{fallbackText(f.label, '')}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ position: 'relative', overflow: 'hidden', minHeight: isMobile ? 280 : undefined, order: isMobile ? 1 : 0 }}>
|
||||
<Image
|
||||
unoptimized
|
||||
src={mediaUrl(prize.image, `/images/${aboutContent.prize.imageFilename}`)}
|
||||
alt={mediaAlt(prize.image, fallbackText(prize.imageAlt, aboutContent.prize.imageAlt))}
|
||||
style={{ position: 'absolute', inset: 0, width: '100%', height: '100%', objectFit: 'cover', objectPosition: 'center', display: 'block' }}
|
||||
/>
|
||||
<div style={{ position: 'absolute', inset: 0, background: isMobile
|
||||
? 'linear-gradient(to top, rgba(3,9,58,0.94) 0%, rgba(3,9,58,0.55) 38%, rgba(3,9,58,0.12) 72%, transparent 100%), linear-gradient(to right, rgba(3,9,58,0.5) 0%, transparent 55%)'
|
||||
: `linear-gradient(to right, ${WHITE} 0%, transparent 25%), linear-gradient(to top, rgba(3,9,58,0.75) 0%, transparent 60%)` }} />
|
||||
{isMobile && (
|
||||
<div style={{ position: 'absolute', left: 0, right: 0, bottom: 0, padding: '0 24px 24px', zIndex: 2 }}>
|
||||
<span style={{ fontFamily: FF, fontSize: 10, color: '#8FBEEC', textTransform: 'uppercase', letterSpacing: '0.32em', fontWeight: 700, display: 'block', marginBottom: 12 }}>{fallbackText(prize.eyebrow, aboutContent.prize.eyebrow)}</span>
|
||||
<h2 className="type-section-title text-role-inverse" style={{ textTransform: 'uppercase', margin: 0, textShadow: '0 2px 18px rgba(3,9,58,0.6)' }}>
|
||||
<Lines text={fallbackText(prize.heading, aboutContent.prize.heading)} />
|
||||
</h2>
|
||||
</div>
|
||||
)}
|
||||
{!isMobile && (
|
||||
<div style={{ position: 'absolute', bottom: 36, left: 36 }}>
|
||||
<div style={{ fontFamily: FF, fontSize: 10, fontWeight: 700, letterSpacing: '0.22em', textTransform: 'uppercase', color: GOLD, marginBottom: 6 }}>{fallbackText(prize.imageLabel, aboutContent.prize.imageLabel)}</div>
|
||||
<div style={{ width: 28, height: 2, background: GOLD }} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<figure data-testid="prize-figure" style={{ position: 'relative', overflow: 'hidden', minHeight: isMobile ? 280 : isRelevanceStacked ? 420 : undefined, margin: 0, order: isRelevanceStacked ? 1 : 0, display: isMobile ? 'flex' : 'block', flexDirection: isMobile ? 'column' : undefined }}>
|
||||
<div data-testid="prize-image-visual" style={{ position: isMobile ? 'relative' : 'absolute', inset: isMobile ? undefined : 0, minHeight: isMobile ? 280 : undefined, overflow: 'hidden' }}>
|
||||
<Image
|
||||
unoptimized
|
||||
data-testid="prize-image"
|
||||
src={mediaUrl(prize.image, `/images/${aboutContent.prize.imageFilename}`)}
|
||||
alt={mediaAlt(prize.image, fallbackText(prize.imageAlt, aboutContent.prize.imageAlt))}
|
||||
style={{ position: 'absolute', inset: 0, width: '100%', height: '100%', objectFit: 'cover', objectPosition: mediaObjectPosition(prize.image), display: 'block' }}
|
||||
/>
|
||||
<div style={{ position: 'absolute', inset: 0, background: isMobile
|
||||
? 'linear-gradient(to top, rgba(3,9,58,0.94) 0%, rgba(3,9,58,0.55) 38%, rgba(3,9,58,0.12) 72%, transparent 100%), linear-gradient(to right, rgba(3,9,58,0.5) 0%, transparent 55%)'
|
||||
: `linear-gradient(to right, ${WHITE} 0%, transparent 25%), linear-gradient(to top, rgba(3,9,58,0.58) 0%, transparent 52%)` }} />
|
||||
{isMobile && (
|
||||
<div style={{ position: 'absolute', left: 0, right: 0, bottom: 0, padding: '0 24px 24px', zIndex: 2 }}>
|
||||
<span style={{ fontFamily: FF, fontSize: 10, color: '#8FBEEC', textTransform: 'uppercase', letterSpacing: '0.32em', fontWeight: 700, display: 'block', marginBottom: 12 }}>{fallbackText(prize.eyebrow, aboutContent.prize.eyebrow)}</span>
|
||||
<h2 className="type-section-title text-role-inverse" style={{ textTransform: 'uppercase', margin: 0, textShadow: '0 2px 18px rgba(3,9,58,0.6)' }}>
|
||||
<Lines text={fallbackText(prize.heading, aboutContent.prize.heading)} />
|
||||
</h2>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<figcaption data-testid="prize-image-caption" style={{ position: isMobile ? 'relative' : 'absolute', zIndex: 3, bottom: isMobile ? undefined : 32, left: isMobile ? undefined : 32, maxWidth: isMobile ? undefined : 'calc(100% - 64px)', padding: isMobile ? '12px 24px' : '10px 14px', borderLeft: `3px solid ${GOLD}`, background: isMobile ? '#03093A' : 'rgba(3,9,58,0.92)', color: '#fff', fontFamily: FF, fontSize: 11, fontWeight: 700, lineHeight: 1.4, letterSpacing: '0.16em', textTransform: 'uppercase', overflowWrap: 'anywhere', boxShadow: isMobile ? 'none' : '0 8px 24px rgba(3,9,58,0.24)', backdropFilter: isMobile ? undefined : 'blur(4px)' }}>
|
||||
{fallbackText(prize.imageLabel, aboutContent.prize.imageLabel)}
|
||||
</figcaption>
|
||||
</figure>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
||||
170
tests/int/about-prize.int.spec.tsx
Normal file
170
tests/int/about-prize.int.spec.tsx
Normal file
@@ -0,0 +1,170 @@
|
||||
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 About from '@/spa/pages/About'
|
||||
|
||||
const viewport = vi.hoisted(() => ({ mobile: false, stacked: false }))
|
||||
|
||||
vi.mock('@/spa/hooks/useIsMobile', () => ({
|
||||
useIsMobile: (breakpoint = 768) => (breakpoint === 1024 ? viewport.stacked : viewport.mobile),
|
||||
}))
|
||||
|
||||
vi.mock('next/navigation', () => ({
|
||||
usePathname: () => '/der-bmp',
|
||||
useRouter: () => ({ push: vi.fn(), replace: vi.fn() }),
|
||||
}))
|
||||
|
||||
vi.mock('@/spa/components/AwardsGridSection', () => ({
|
||||
default: () => <section data-testid="awards-stub" />,
|
||||
}))
|
||||
|
||||
vi.mock('@/spa/components/HomeWinnersSection', () => ({
|
||||
default: () => <section data-testid="winners-stub" />,
|
||||
}))
|
||||
|
||||
vi.mock('@/spa/components/TestimonialsSection', () => ({
|
||||
default: () => <section data-testid="testimonials-stub" />,
|
||||
}))
|
||||
|
||||
vi.mock('@/spa/components/ui/munich-skyline-bg', () => ({
|
||||
default: () => null,
|
||||
}))
|
||||
|
||||
const aboutRoute: CmsRouteData = {
|
||||
collection: 'pages',
|
||||
doc: {
|
||||
id: 'about-page',
|
||||
slug: 'der-bmp',
|
||||
spaPath: '/der-bmp',
|
||||
about: {
|
||||
prize: {
|
||||
image: {
|
||||
url: '/api/media/file/cms-prize.jpg',
|
||||
alt: 'CMS prize ceremony',
|
||||
focalX: 32,
|
||||
focalY: 68,
|
||||
},
|
||||
imageAlt: 'Page-owned prize fallback alt',
|
||||
eyebrow: 'CMS prize eyebrow',
|
||||
heading: 'CMS PRIZE\nHEADING',
|
||||
body: [{ text: 'First CMS prize paragraph.' }, { text: 'Second CMS prize paragraph.' }],
|
||||
facts: [
|
||||
{ value: 'A', label: 'Founded' },
|
||||
{
|
||||
value: 'B',
|
||||
label: 'An intentionally very long CMS-owned fact label that must remain readable',
|
||||
},
|
||||
{ value: 'C', label: 'Independent' },
|
||||
],
|
||||
imageLabel: 'CMS-owned prize image caption',
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
const renderAbout = () =>
|
||||
render(
|
||||
<CmsRouteProvider value={aboutRoute}>
|
||||
<About />
|
||||
</CmsRouteProvider>,
|
||||
)
|
||||
|
||||
describe('/der-bmp prize facts and caption', () => {
|
||||
beforeEach(() => {
|
||||
viewport.mobile = false
|
||||
viewport.stacked = false
|
||||
})
|
||||
|
||||
afterEach(() => cleanup())
|
||||
|
||||
it('centers three equal fact cells and keeps full-height separators between them', () => {
|
||||
renderAbout()
|
||||
|
||||
const facts = screen.getByTestId('prize-facts')
|
||||
const cells = within(facts).getAllByTestId('prize-fact')
|
||||
|
||||
expect(facts.style.display).toBe('grid')
|
||||
expect(facts.style.gridTemplateColumns).toBe('repeat(3, minmax(0, 1fr))')
|
||||
expect(cells).toHaveLength(3)
|
||||
expect(cells.every((cell) => cell.style.minWidth === '0')).toBe(true)
|
||||
expect(cells.every((cell) => cell.style.alignItems === 'center')).toBe(true)
|
||||
expect(cells.every((cell) => cell.style.textAlign === 'center')).toBe(true)
|
||||
expect(cells.slice(0, 2).every((cell) => cell.style.borderRightWidth === '1px')).toBe(true)
|
||||
expect(cells[2]?.style.borderRightStyle).toBe('none')
|
||||
})
|
||||
|
||||
it('preserves long CMS fact labels without allowing them to escape their equal cells', () => {
|
||||
renderAbout()
|
||||
|
||||
const labels = screen.getAllByTestId('prize-fact-label')
|
||||
const longLabel = screen.getByText(
|
||||
'An intentionally very long CMS-owned fact label that must remain readable',
|
||||
)
|
||||
|
||||
expect(labels.map((label) => label.textContent)).toEqual([
|
||||
'Founded',
|
||||
'An intentionally very long CMS-owned fact label that must remain readable',
|
||||
'Independent',
|
||||
])
|
||||
expect(labels.every((label) => label.style.maxWidth === '100%')).toBe(true)
|
||||
expect(labels.every((label) => label.style.overflowWrap === 'anywhere')).toBe(true)
|
||||
expect(longLabel.style.hyphens).toBe('auto')
|
||||
})
|
||||
|
||||
it('keeps CMS media, alt text, focal point, and caption in a semantic contrast container', () => {
|
||||
renderAbout()
|
||||
|
||||
const section = screen.getByTestId('prize-section')
|
||||
const figure = within(section).getByTestId('prize-figure')
|
||||
const image = within(section).getByRole('img', { name: 'CMS prize ceremony' })
|
||||
const caption = within(section).getByTestId('prize-image-caption')
|
||||
|
||||
expect(figure.tagName).toBe('FIGURE')
|
||||
expect(caption.tagName).toBe('FIGCAPTION')
|
||||
expect(image.getAttribute('src')).toBe('/api/media/file/cms-prize.jpg')
|
||||
expect(image.style.objectPosition).toBe('32% 68%')
|
||||
expect(caption.textContent).toBe('CMS-owned prize image caption')
|
||||
expect(caption.style.background).toBe('rgba(3, 9, 58, 0.92)')
|
||||
expect(caption.style.color).toBe('rgb(255, 255, 255)')
|
||||
expect(caption.style.borderLeftWidth).toBe('3px')
|
||||
expect(image.compareDocumentPosition(caption) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy()
|
||||
})
|
||||
|
||||
it('stacks the image first at tablet width while retaining equal fact structure', () => {
|
||||
viewport.stacked = true
|
||||
renderAbout()
|
||||
|
||||
const layout = screen.getByTestId('prize-layout')
|
||||
const figure = screen.getByTestId('prize-figure')
|
||||
const copy = screen.getByTestId('prize-copy-panel')
|
||||
|
||||
expect(layout.getAttribute('data-layout')).toBe('stacked')
|
||||
expect(layout.style.gridTemplateColumns).toBe('1fr')
|
||||
expect(figure.style.order).toBe('1')
|
||||
expect(figure.style.minHeight).toBe('420px')
|
||||
expect(copy.style.order).toBe('2')
|
||||
expect(copy.style.padding).toBe('56px 48px')
|
||||
expect(within(copy).getAllByTestId('prize-fact')).toHaveLength(3)
|
||||
})
|
||||
|
||||
it('uses a dedicated mobile caption area without changing CMS copy or media order', () => {
|
||||
viewport.mobile = true
|
||||
viewport.stacked = true
|
||||
renderAbout()
|
||||
|
||||
const figure = screen.getByTestId('prize-figure')
|
||||
const visual = screen.getByTestId('prize-image-visual')
|
||||
const caption = screen.getByTestId('prize-image-caption')
|
||||
const copy = screen.getByTestId('prize-copy-panel')
|
||||
|
||||
expect(figure.style.display).toBe('flex')
|
||||
expect(visual.style.minHeight).toBe('280px')
|
||||
expect(caption.style.position).toBe('relative')
|
||||
expect(caption.style.background).toBe('rgb(3, 9, 58)')
|
||||
expect(caption.textContent).toBe('CMS-owned prize image caption')
|
||||
expect(copy.compareDocumentPosition(figure) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy()
|
||||
expect(screen.getByText('First CMS prize paragraph.')).toBeTruthy()
|
||||
expect(screen.queryByText('BMP Preisverleihung · München')).toBeNull()
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user