feat(network): add responsive sponsor hierarchy
This commit is contained in:
183
tests/int/netzwerk-sponsor-hierarchy.int.spec.tsx
Normal file
183
tests/int/netzwerk-sponsor-hierarchy.int.spec.tsx
Normal file
@@ -0,0 +1,183 @@
|
||||
import { cleanup, render, screen, within } from '@testing-library/react'
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { join } from 'node:path'
|
||||
import { afterEach, beforeAll, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { CmsRouteProvider, type CmsRouteData } from '@/spa/cmsRoute'
|
||||
import { netzwerkContent } from '@/spa/netzwerkContent'
|
||||
import Netzwerk from '@/spa/pages/Netzwerk'
|
||||
import {
|
||||
groupSponsorsByTier,
|
||||
sponsorCardBasis,
|
||||
sponsorColumns,
|
||||
sponsorViewport,
|
||||
} from '@/spa/sponsorHierarchy'
|
||||
|
||||
vi.mock('next/navigation', () => ({
|
||||
usePathname: () => '/netzwerk',
|
||||
useRouter: () => ({ push: vi.fn(), replace: vi.fn() }),
|
||||
}))
|
||||
|
||||
let viewportWidth = 1440
|
||||
|
||||
const partner = (name: string, tier: 1 | 2 | 3, sortOrder: number) => ({
|
||||
id: name.toLowerCase().replace(/\s/g, '-'),
|
||||
stableId: name.toLowerCase().replace(/\s/g, '-'),
|
||||
name,
|
||||
tier,
|
||||
sortOrder,
|
||||
active: true,
|
||||
desc: `${name} description`,
|
||||
fullDesc: `${name} details`,
|
||||
industry: `${name} industry`,
|
||||
location: `${name} location`,
|
||||
website: `${name.toLowerCase()}.example`,
|
||||
})
|
||||
|
||||
const partners = [
|
||||
partner('Tier One B', 1, 2),
|
||||
partner('Tier Three H', 3, 8),
|
||||
partner('Tier Two B', 2, 2),
|
||||
partner('Tier Three A', 3, 1),
|
||||
partner('Tier One A', 1, 1),
|
||||
partner('Tier Three C', 3, 3),
|
||||
partner('Tier Two A', 2, 1),
|
||||
partner('Tier Three B', 3, 2),
|
||||
partner('Tier Three D', 3, 4),
|
||||
partner('Tier Three E', 3, 5),
|
||||
partner('Tier Three F', 3, 6),
|
||||
partner('Tier Three G', 3, 7),
|
||||
]
|
||||
|
||||
const sponsorCopy = {
|
||||
eyebrow: 'CMS sponsor eyebrow',
|
||||
heading: 'CMS sponsor heading',
|
||||
description: 'CMS sponsor supporting copy',
|
||||
detailLabel: 'CMS sponsor action',
|
||||
tiers: [
|
||||
{ tier: 1, label: 'CMS feature tier', note: 'CMS feature tier note' },
|
||||
{ tier: 2, label: 'CMS standard tier', note: 'CMS standard tier note' },
|
||||
{ tier: 3, label: 'CMS compact tier', note: 'CMS compact tier note' },
|
||||
],
|
||||
}
|
||||
|
||||
const routeData = (): CmsRouteData => ({
|
||||
collection: 'pages',
|
||||
doc: {
|
||||
id: 'network-page',
|
||||
slug: 'netzwerk',
|
||||
spaPath: '/netzwerk',
|
||||
netzwerk: { partners: sponsorCopy },
|
||||
},
|
||||
lists: {
|
||||
partners,
|
||||
'jury-mitglieder': [],
|
||||
},
|
||||
})
|
||||
|
||||
function renderNetwork(width: number) {
|
||||
viewportWidth = width
|
||||
return render(
|
||||
<CmsRouteProvider value={routeData()}>
|
||||
<Netzwerk />
|
||||
</CmsRouteProvider>,
|
||||
)
|
||||
}
|
||||
|
||||
describe('responsive sponsor hierarchy', () => {
|
||||
beforeAll(() => {
|
||||
Object.defineProperty(window, 'matchMedia', {
|
||||
configurable: true,
|
||||
value: vi.fn().mockImplementation((query: string) => {
|
||||
const maximum = Number(query.match(/max-width:\s*(\d+)px/)?.[1] || Number.POSITIVE_INFINITY)
|
||||
return {
|
||||
addEventListener: vi.fn(),
|
||||
matches: viewportWidth <= maximum,
|
||||
media: query,
|
||||
removeEventListener: vi.fn(),
|
||||
}
|
||||
}),
|
||||
})
|
||||
})
|
||||
|
||||
afterEach(() => cleanup())
|
||||
|
||||
it('groups by Partner tier and sorts each band by Partner sortOrder', () => {
|
||||
const grouped = groupSponsorsByTier(partners)
|
||||
|
||||
expect(grouped[1].map(({ name }) => name)).toEqual(['Tier One A', 'Tier One B'])
|
||||
expect(grouped[2].map(({ name }) => name)).toEqual(['Tier Two A', 'Tier Two B'])
|
||||
expect(grouped[3].map(({ name }) => name)).toEqual([
|
||||
'Tier Three A',
|
||||
'Tier Three B',
|
||||
'Tier Three C',
|
||||
'Tier Three D',
|
||||
'Tier Three E',
|
||||
'Tier Three F',
|
||||
'Tier Three G',
|
||||
'Tier Three H',
|
||||
])
|
||||
})
|
||||
|
||||
it('defines the approved columns and conservative narrow-mobile fallback', () => {
|
||||
expect([1, 2, 3].map((tier) => sponsorColumns(tier as 1 | 2 | 3, 'desktop'))).toEqual([2, 3, 4])
|
||||
expect([1, 2, 3].map((tier) => sponsorColumns(tier as 1 | 2 | 3, 'tablet'))).toEqual([2, 2, 3])
|
||||
expect([1, 2, 3].map((tier) => sponsorColumns(tier as 1 | 2 | 3, 'mobile'))).toEqual([1, 1, 2])
|
||||
expect([1, 2, 3].map((tier) => sponsorColumns(tier as 1 | 2 | 3, 'narrow-mobile'))).toEqual([1, 1, 1])
|
||||
expect(sponsorViewport({ isMobile: false, isNarrowMobile: false, isTabletOrMobile: true })).toBe('tablet')
|
||||
expect(sponsorCardBasis(3, 16)).toBe('calc((100% - 32px) / 3)')
|
||||
})
|
||||
|
||||
it('renders every visible sponsor-section label from Netzwerk CMS fields', () => {
|
||||
renderNetwork(1440)
|
||||
|
||||
expect(screen.getByText(sponsorCopy.eyebrow)).toBeTruthy()
|
||||
expect(screen.getByText(sponsorCopy.heading)).toBeTruthy()
|
||||
expect(screen.getByText(sponsorCopy.description)).toBeTruthy()
|
||||
sponsorCopy.tiers.forEach(({ label, note }) => {
|
||||
expect(screen.getByRole('heading', { name: label })).toBeTruthy()
|
||||
expect(screen.getByText(note)).toBeTruthy()
|
||||
})
|
||||
expect(screen.getAllByText(sponsorCopy.detailLabel)).toHaveLength(partners.length)
|
||||
expect(screen.queryByText(netzwerkContent.partners.description)).toBeNull()
|
||||
netzwerkContent.partners.tiers.forEach(({ label }) => expect(screen.queryByText(label)).toBeNull())
|
||||
})
|
||||
|
||||
it.each([
|
||||
{ width: 1440, viewport: 'desktop', columns: [2, 3, 4] },
|
||||
{ width: 1024, viewport: 'tablet', columns: [2, 2, 3] },
|
||||
{ width: 390, viewport: 'mobile', columns: [1, 1, 2] },
|
||||
{ width: 340, viewport: 'narrow-mobile', columns: [1, 1, 1] },
|
||||
])('centers independent bands at $width px', ({ width, viewport, columns }) => {
|
||||
renderNetwork(width)
|
||||
|
||||
columns.forEach((columnCount, index) => {
|
||||
const tier = index + 1
|
||||
const band = screen.getByTestId(`sponsor-tier-${tier}`)
|
||||
const cards = screen.getByTestId(`sponsor-tier-${tier}-cards`)
|
||||
expect(band.getAttribute('data-columns')).toBe(String(columnCount))
|
||||
expect(band.getAttribute('data-viewport')).toBe(viewport)
|
||||
expect(cards.style.display).toBe('flex')
|
||||
expect(cards.style.flexWrap).toBe('wrap')
|
||||
expect(cards.style.justifyContent).toBe('center')
|
||||
expect(within(band).getAllByRole('button')).toHaveLength(tier === 3 ? 8 : 2)
|
||||
})
|
||||
})
|
||||
|
||||
it('keeps sponsor defaults and legacy labels out of the rendered component source', () => {
|
||||
const source = readFileSync(join(process.cwd(), 'src/spa/pages/Netzwerk.tsx'), 'utf8')
|
||||
const visibleDefaults = [
|
||||
netzwerkContent.partners.eyebrow,
|
||||
netzwerkContent.partners.heading,
|
||||
netzwerkContent.partners.description,
|
||||
netzwerkContent.partners.detailLabel,
|
||||
...netzwerkContent.partners.tiers.map(({ label }) => label),
|
||||
'Partner in drei Kategorien: Hauptsponsoren, Medienpartner und weitere Sponsoren. Klicken für Details.',
|
||||
]
|
||||
|
||||
visibleDefaults.forEach((copy) => {
|
||||
expect(source).not.toContain(`'${copy}'`)
|
||||
expect(source).not.toContain(`"${copy}"`)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -106,7 +106,13 @@ describe('Payload field inventory', () => {
|
||||
'netzwerk.patronage.greetingVideoTitle',
|
||||
'netzwerk.patronage.greetingVideoDescription',
|
||||
'netzwerk.patronage.greetingVideoCloseLabel',
|
||||
'netzwerk.partners.eyebrow',
|
||||
'netzwerk.partners.heading',
|
||||
'netzwerk.partners.description',
|
||||
'netzwerk.partners.detailLabel',
|
||||
'netzwerk.partners.tiers',
|
||||
'netzwerk.partners.tiers.label',
|
||||
'netzwerk.partners.tiers.note',
|
||||
'netzwerk.partners.modal.websiteLabel',
|
||||
'netzwerk.sponsoringForm.steps',
|
||||
'netzwerk.sponsoringForm.requiredFieldError',
|
||||
@@ -153,6 +159,15 @@ describe('Payload field inventory', () => {
|
||||
expect(field).not.toHaveProperty('defaultValue')
|
||||
})
|
||||
|
||||
it('keeps sponsor tier copy separate from collection-owned tier assignment and ordering', () => {
|
||||
expect(findField(netzwerkFields, 'netzwerk.partners.tiers')).toMatchObject({
|
||||
type: 'array',
|
||||
dbName: 'network_tiers',
|
||||
})
|
||||
expect(findField(netzwerkFields, 'netzwerk.partners.tiers.label')).toMatchObject({ type: 'text' })
|
||||
expect(findField(netzwerkFields, 'netzwerk.partners.tiers.note')).toMatchObject({ type: 'text' })
|
||||
})
|
||||
|
||||
it('limits the press contact upload to image media', () => {
|
||||
const field = findField(pressIndexFields, 'pressIndex.downloads.contactImage')
|
||||
|
||||
|
||||
Reference in New Issue
Block a user