diff --git a/dotcom-rendering/src/components/PuzzlesDirectory.test.tsx b/dotcom-rendering/src/components/PuzzlesDirectory.test.tsx index 9cee38c9234..46f3d16fdb3 100644 --- a/dotcom-rendering/src/components/PuzzlesDirectory.test.tsx +++ b/dotcom-rendering/src/components/PuzzlesDirectory.test.tsx @@ -8,11 +8,6 @@ import type { } from '../types/puzzlesPage'; import { getPuzzleUrl, PuzzlesDirectory } from './PuzzlesDirectory'; -jest.mock('./AdSlot.web', () => ({ - AdSlot: ({ index }: { index: number }) => ( -
- ), -})); jest.mock('./Island', () => ({ Island: ({ children }: { children: ReactNode }) => children, })); @@ -38,6 +33,36 @@ const section = ( }); describe('PuzzlesDirectory', () => { + it('renders unique desktop and mobile IDs for multiple blueprint slots', () => { + const layout: PuzzlesLayoutType = { + containers: ['inline1', 'inline2'].map((adSlot) => + section({ + id: adSlot, + title: '', + variant: 'ad', + adSlot, + content: { items: [], nestedContainers: [] }, + }), + ), + }; + const { rerender } = render( + , + ); + const ids = Array.from( + document.querySelectorAll('.js-ad-slot'), + ({ id }) => id, + ); + expect(ids).toEqual([ + 'dfp-ad--fronts-banner-1', + 'dfp-ad--inline1--mobile', + 'dfp-ad--fronts-banner-2', + 'dfp-ad--inline2--mobile', + ]); + expect(new Set(ids).size).toBe(ids.length); + rerender(); + expect(document.querySelector('.js-ad-slot')).not.toBeInTheDocument(); + }); + it('does not render a disabled featured container', () => { render( { screen.queryByRole('heading', { name: 'Empty' }), ).not.toBeInTheDocument(); expect(document.querySelector('img')).not.toBeInTheDocument(); - expect(screen.queryByTestId('ad-2')).not.toBeInTheDocument(); + expect(document.querySelector('.js-ad-slot')).not.toBeInTheDocument(); rerender(); - expect(screen.getByTestId('ad-2')).toBeInTheDocument(); + expect( + document.getElementById('dfp-ad--fronts-banner-2'), + ).toHaveAttribute('data-name', 'fronts-banner-2'); + expect( + document.getElementById('dfp-ad--inline2--mobile'), + ).toHaveAttribute('data-name', 'inline2'); }); it('renders the archive dropdown and closes it with Escape or an outside click', async () => { diff --git a/dotcom-rendering/src/components/PuzzlesDirectory.tsx b/dotcom-rendering/src/components/PuzzlesDirectory.tsx index a4736e7a9e8..acd5d476df7 100644 --- a/dotcom-rendering/src/components/PuzzlesDirectory.tsx +++ b/dotcom-rendering/src/components/PuzzlesDirectory.tsx @@ -416,6 +416,7 @@ export const PuzzlesDirectory = ({ layout, renderAds }: Props) => ( index={index} position="fronts-banner" /> +
); } diff --git a/dotcom-rendering/src/layouts/PuzzlesLayout.test.tsx b/dotcom-rendering/src/layouts/PuzzlesLayout.test.tsx index aecafa32296..ef796d8400c 100644 --- a/dotcom-rendering/src/layouts/PuzzlesLayout.test.tsx +++ b/dotcom-rendering/src/layouts/PuzzlesLayout.test.tsx @@ -6,9 +6,6 @@ import { PuzzlesLayout } from './PuzzlesLayout'; jest.mock('../components/Masthead/Masthead', () => ({ Masthead: () =>
, })); -jest.mock('../components/HeaderAdSlot', () => ({ - HeaderAdSlot: () =>
, -})); jest.mock('../components/Footer', () => ({ Footer: () =>
, })); @@ -51,12 +48,17 @@ describe('PuzzlesLayout', () => { const { rerender } = render( , ); - expect(screen.queryByTestId('header-ad')).not.toBeInTheDocument(); + expect(document.querySelector('.js-ad-slot')).not.toBeInTheDocument(); expect(screen.getByTestId('masthead')).toBeInTheDocument(); expect(screen.getByTestId('footer')).toBeInTheDocument(); rerender( , ); - expect(screen.getByTestId('header-ad')).toBeInTheDocument(); + expect( + document.getElementById('dfp-ad--top-above-nav'), + ).toBeInTheDocument(); + expect( + document.getElementById('dfp-ad--mobile-above-nav'), + ).not.toBeInTheDocument(); }); }); diff --git a/dotcom-rendering/src/model/validate.puzzlesPage.test.ts b/dotcom-rendering/src/model/validate.puzzlesPage.test.ts index d28f902092b..141f482d64b 100644 --- a/dotcom-rendering/src/model/validate.puzzlesPage.test.ts +++ b/dotcom-rendering/src/model/validate.puzzlesPage.test.ts @@ -38,6 +38,68 @@ const validPage = () => ({ }); describe('validateAsPuzzlesPageType', () => { + it.each(['inline1', 'mostpop'])( + 'rejects repeated %s slot names', + (adSlot) => { + const page = validPage(); + const container = { + id: 'first-ad', + title: '', + variant: adSlot === 'mostpop' ? 'supporting' : 'ad', + adSlot, + content: { items: [], nestedContainers: [] }, + ...(adSlot === 'mostpop' + ? { + supporting: { + usefulLinksTitle: 'Useful links', + usefulLinks: [], + popularTitle: 'Most popular puzzles', + popularGroups: [], + }, + } + : {}), + }; + page.layout.containers.push(container as never); + expect(validateAsPuzzlesPageType(page)).toBeDefined(); + page.layout.containers.push({ + ...container, + id: 'second-ad', + } as never); + expect(() => validateAsPuzzlesPageType(page)).toThrow( + 'Unable to validate request body for puzzles page', + ); + }, + ); + + it.each(['inline0', 'inline-1', 'hub-inline', 'inline1junk', undefined])( + 'rejects an unsupported ad slot: %s', + (adSlot) => { + const page = validPage(); + page.layout.containers.push({ + id: 'invalid-ad', + title: '', + variant: 'ad', + adSlot, + content: { items: [], nestedContainers: [] }, + } as never); + expect(() => validateAsPuzzlesPageType(page)).toThrow(); + }, + ); + + it('accepts distinct inline slot names', () => { + const page = validPage(); + for (const adSlot of ['inline1', 'inline2']) { + page.layout.containers.push({ + id: adSlot, + title: '', + variant: 'ad', + adSlot, + content: { items: [], nestedContainers: [] }, + } as never); + } + expect(validateAsPuzzlesPageType(page)).toBeDefined(); + }); + it('accepts a valid recursive blueprint contract', () => { expect( validateAsPuzzlesPageType(validPage()).layout.containers[0]?.id, diff --git a/dotcom-rendering/src/model/validate.ts b/dotcom-rendering/src/model/validate.ts index f958421ea0c..60480796519 100644 --- a/dotcom-rendering/src/model/validate.ts +++ b/dotcom-rendering/src/model/validate.ts @@ -354,6 +354,9 @@ export const validateAsPuzzlesPageType = (data: unknown): FEPuzzlesPageType => { const unique = (values: string[]) => new Set(values).size === values.length; const containerIds = containers.map(({ id }) => id); const itemIds = items.map(({ id }) => id); + const adSlots = containers.flatMap(({ adSlot }) => + adSlot === undefined ? [] : [adSlot], + ); const popularReferencesValid = containers.every((container) => (container.supporting?.popularGroups ?? []).every((group) => group.itemIds.every((id) => itemIds.includes(id)), @@ -366,6 +369,7 @@ export const validateAsPuzzlesPageType = (data: unknown): FEPuzzlesPageType => { if ( !unique(containerIds) || !unique(itemIds) || + !unique(adSlots) || !popularReferencesValid || !topLevelOnlyContainersValid ) {