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
) {