From 6b4a4c1cdd84bf998fd55669e45fade9f96d746d Mon Sep 17 00:00:00 2001 From: Demetrios Date: Mon, 22 Dec 2025 09:53:18 +0000 Subject: [PATCH 1/7] Load mobile sticky ads only when the reader revenue banner is not present --- bundle/src/insert/mobile-sticky.ts | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/bundle/src/insert/mobile-sticky.ts b/bundle/src/insert/mobile-sticky.ts index b742a758d..d8460d493 100644 --- a/bundle/src/insert/mobile-sticky.ts +++ b/bundle/src/insert/mobile-sticky.ts @@ -1,3 +1,4 @@ +import { log } from '@guardian/libs'; import { createAdSlot } from '../lib/create-ad-slot'; import fastdom from '../lib/fastdom-promise'; import { shouldIncludeMobileSticky } from '../lib/header-bidding/utils'; @@ -33,15 +34,17 @@ const createAdWrapper = () => { */ export const init = (): Promise => { if (shouldIncludeMobileSticky()) { - const mobileStickyWrapper = createAdWrapper(); - return fastdom - .mutate(() => { - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- Is body really always defined? - if (document.body && mobileStickyWrapper) { - document.body.appendChild(mobileStickyWrapper); - } - }) - .then(() => { + document.addEventListener('banner:close', () => { + void (async () => { + log('commercial', '🪵 Supporter revenue banner closed'); + const mobileStickyWrapper = createAdWrapper(); + + await fastdom.mutate(() => { + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- Is body really always defined? + if (document.body && mobileStickyWrapper) { + document.body.appendChild(mobileStickyWrapper); + } + }); if (mobileStickyWrapper) { const mobileStickyAdSlot = mobileStickyWrapper.querySelector( @@ -51,7 +54,8 @@ export const init = (): Promise => { void fillDynamicAdSlot(mobileStickyAdSlot, true); } } - }); + })(); + }); } return Promise.resolve(); From 9378857106b497a878d3eba5bfda7febff7c0705 Mon Sep 17 00:00:00 2001 From: Demetrios Date: Mon, 29 Dec 2025 16:54:48 +0000 Subject: [PATCH 2/7] refactor logic to render mobile sticky slot as discrete function --- bundle/src/insert/mobile-sticky.ts | 53 +++++++++++++++++------------- 1 file changed, 30 insertions(+), 23 deletions(-) diff --git a/bundle/src/insert/mobile-sticky.ts b/bundle/src/insert/mobile-sticky.ts index d8460d493..e3b084b9d 100644 --- a/bundle/src/insert/mobile-sticky.ts +++ b/bundle/src/insert/mobile-sticky.ts @@ -32,30 +32,37 @@ const createAdWrapper = () => { * Initialise mobile sticky ad slot * @returns Promise */ -export const init = (): Promise => { + + +const renderMobileStickySlot = async () => { + log('commercial', '🪵 Rendering MobileSticky'); + const mobileStickyWrapper = createAdWrapper(); + await fastdom.mutate(() => { + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- Is body really always defined? + if (document.body && mobileStickyWrapper) { + document.body.appendChild(mobileStickyWrapper); + } + }); + if (mobileStickyWrapper) { + const mobileStickyAdSlot = + mobileStickyWrapper.querySelector( + '#dfp-ad--mobile-sticky', + ); + if (mobileStickyAdSlot) { + void fillDynamicAdSlot(mobileStickyAdSlot, true); + } + } +}; + +export const init = (): Promise => { + const handleBannerEvent = () => { + log('commercial', '🪵 Handle Banner Event'); + void renderMobileStickySlot(); + }; + if (shouldIncludeMobileSticky()) { - document.addEventListener('banner:close', () => { - void (async () => { - log('commercial', '🪵 Supporter revenue banner closed'); - const mobileStickyWrapper = createAdWrapper(); - - await fastdom.mutate(() => { - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- Is body really always defined? - if (document.body && mobileStickyWrapper) { - document.body.appendChild(mobileStickyWrapper); - } - }); - if (mobileStickyWrapper) { - const mobileStickyAdSlot = - mobileStickyWrapper.querySelector( - '#dfp-ad--mobile-sticky', - ); - if (mobileStickyAdSlot) { - void fillDynamicAdSlot(mobileStickyAdSlot, true); - } - } - })(); - }); + document.addEventListener('banner:close', handleBannerEvent); + document.addEventListener('banner:none', handleBannerEvent); } return Promise.resolve(); From ee8ea2289afa8267157f3bcdcddca37e81f0f0e9 Mon Sep 17 00:00:00 2001 From: Demetrios Date: Wed, 7 Jan 2026 11:39:49 +0000 Subject: [PATCH 3/7] add playright test for response to events from dcr --- bundle/playwright/tests/mobile-sticky-slot.ts | 65 +++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 bundle/playwright/tests/mobile-sticky-slot.ts diff --git a/bundle/playwright/tests/mobile-sticky-slot.ts b/bundle/playwright/tests/mobile-sticky-slot.ts new file mode 100644 index 000000000..22b556f39 --- /dev/null +++ b/bundle/playwright/tests/mobile-sticky-slot.ts @@ -0,0 +1,65 @@ +import { expect, test } from '@playwright/test'; +import { testAtBreakpoints } from '../lib/breakpoints'; +import { loadPage } from '../lib/load-page'; +import { cmpAcceptAll } from '../lib/cmp'; +import { articles } from '../fixtures/pages'; +import { GuPage } from '../fixtures/pages/Page'; + +const { path } = articles[0] as unknown as GuPage; + +testAtBreakpoints(['mobile']).forEach(({ breakpoint, width, height }) => { + test(`mobile sticky responds to banner:close event at ${breakpoint}`, async ({ + page, + }) => { + await page.setViewportSize({ width, height }); + + await page.addInitScript(() => { + window.localStorage.removeItem( + 'gu.prefs.engagementBannerLastClosedAt', + ); + }); + + await loadPage({ page, path, region: 'US' }); + await cmpAcceptAll(page); + await loadPage({ + page, + path, + region: 'US', + queryParams: { + adtest: 'mobileStickyTest', + }, + }); + + await expect(page.locator('#dfp-ad--mobile-sticky')).not.toBeAttached(); + + await page.evaluate(() => { + document.dispatchEvent(new Event('banner:close')); + }); + + await page.waitForTimeout(1000); + await expect(page.locator('#dfp-ad--mobile-sticky')).toBeAttached(); + }); +}); +testAtBreakpoints(['mobile']).forEach(({ breakpoint, width, height }) => { + test(`mobile sticky responds to banner:none event at ${breakpoint}`, async ({ + page, + }) => { + await page.setViewportSize({ width, height }); + await loadPage({ + page, + path, + region: 'US', + queryParams: { + adtest: 'mobileStickyTest', + }, + }); + + await cmpAcceptAll(page); + await page.evaluate(() => { + document.dispatchEvent(new Event('banner:none')); + }); + + await page.waitForTimeout(1000); + await expect(page.locator('#dfp-ad--mobile-sticky')).toBeVisible(); + }); +}); From c35b5623b714c04c14028a20c1a1644f055a3dfa Mon Sep 17 00:00:00 2001 From: Demetrios Date: Wed, 7 Jan 2026 16:11:53 +0000 Subject: [PATCH 4/7] fix mobile sticky tests to use manual event dispatch --- ...cky-slot.ts => mobile-sticky-slot.spec.ts} | 25 ++++++------------- 1 file changed, 7 insertions(+), 18 deletions(-) rename bundle/playwright/tests/{mobile-sticky-slot.ts => mobile-sticky-slot.spec.ts} (70%) diff --git a/bundle/playwright/tests/mobile-sticky-slot.ts b/bundle/playwright/tests/mobile-sticky-slot.spec.ts similarity index 70% rename from bundle/playwright/tests/mobile-sticky-slot.ts rename to bundle/playwright/tests/mobile-sticky-slot.spec.ts index 22b556f39..29bbf661e 100644 --- a/bundle/playwright/tests/mobile-sticky-slot.ts +++ b/bundle/playwright/tests/mobile-sticky-slot.spec.ts @@ -12,34 +12,23 @@ testAtBreakpoints(['mobile']).forEach(({ breakpoint, width, height }) => { page, }) => { await page.setViewportSize({ width, height }); - - await page.addInitScript(() => { - window.localStorage.removeItem( - 'gu.prefs.engagementBannerLastClosedAt', - ); - }); - await loadPage({ page, path, region: 'US' }); await cmpAcceptAll(page); await loadPage({ page, path, region: 'US', - queryParams: { - adtest: 'mobileStickyTest', - }, + queryParams: { adtest: 'mobileStickyTest' }, }); - await expect(page.locator('#dfp-ad--mobile-sticky')).not.toBeAttached(); - - await page.evaluate(() => { - document.dispatchEvent(new Event('banner:close')); - }); + await page.evaluate(() => { + document.dispatchEvent(new Event('banner:close')); + }); - await page.waitForTimeout(1000); - await expect(page.locator('#dfp-ad--mobile-sticky')).toBeAttached(); + await expect(page.locator('#dfp-ad--mobile-sticky')).toBeVisible(); }); }); + testAtBreakpoints(['mobile']).forEach(({ breakpoint, width, height }) => { test(`mobile sticky responds to banner:none event at ${breakpoint}`, async ({ page, @@ -56,7 +45,7 @@ testAtBreakpoints(['mobile']).forEach(({ breakpoint, width, height }) => { await cmpAcceptAll(page); await page.evaluate(() => { - document.dispatchEvent(new Event('banner:none')); + document.dispatchEvent(new Event('banner:none')); }); await page.waitForTimeout(1000); From ef002cd2bab45de3b6490ca666a167a65b8e3b51 Mon Sep 17 00:00:00 2001 From: Demetrios Date: Wed, 7 Jan 2026 16:20:21 +0000 Subject: [PATCH 5/7] fix linting issues --- bundle/playwright/tests/mobile-sticky-slot.spec.ts | 8 ++++---- bundle/src/insert/mobile-sticky.ts | 5 ++--- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/bundle/playwright/tests/mobile-sticky-slot.spec.ts b/bundle/playwright/tests/mobile-sticky-slot.spec.ts index 29bbf661e..03d826e38 100644 --- a/bundle/playwright/tests/mobile-sticky-slot.spec.ts +++ b/bundle/playwright/tests/mobile-sticky-slot.spec.ts @@ -21,11 +21,11 @@ testAtBreakpoints(['mobile']).forEach(({ breakpoint, width, height }) => { queryParams: { adtest: 'mobileStickyTest' }, }); - await page.evaluate(() => { - document.dispatchEvent(new Event('banner:close')); - }); + await page.evaluate(() => { + document.dispatchEvent(new Event('banner:close')); + }); - await expect(page.locator('#dfp-ad--mobile-sticky')).toBeVisible(); + await expect(page.locator('#dfp-ad--mobile-sticky')).toBeVisible(); }); }); diff --git a/bundle/src/insert/mobile-sticky.ts b/bundle/src/insert/mobile-sticky.ts index e3b084b9d..d2846bb07 100644 --- a/bundle/src/insert/mobile-sticky.ts +++ b/bundle/src/insert/mobile-sticky.ts @@ -33,7 +33,6 @@ const createAdWrapper = () => { * @returns Promise */ - const renderMobileStickySlot = async () => { log('commercial', '🪵 Rendering MobileSticky'); const mobileStickyWrapper = createAdWrapper(); @@ -54,10 +53,10 @@ const renderMobileStickySlot = async () => { } }; -export const init = (): Promise => { +export const init = (): Promise => { const handleBannerEvent = () => { log('commercial', '🪵 Handle Banner Event'); - void renderMobileStickySlot(); + void renderMobileStickySlot(); }; if (shouldIncludeMobileSticky()) { From 1422c34a43b085436cb6f428a0fc117b6cb67f1f Mon Sep 17 00:00:00 2001 From: Demetrios Date: Thu, 8 Jan 2026 11:32:19 +0000 Subject: [PATCH 6/7] refactor mobile sticky tests with realistic banner dismissal flow --- .../tests/mobile-sticky-slot.spec.ts | 29 ++++++++++++------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/bundle/playwright/tests/mobile-sticky-slot.spec.ts b/bundle/playwright/tests/mobile-sticky-slot.spec.ts index 03d826e38..4638747b5 100644 --- a/bundle/playwright/tests/mobile-sticky-slot.spec.ts +++ b/bundle/playwright/tests/mobile-sticky-slot.spec.ts @@ -6,26 +6,36 @@ import { articles } from '../fixtures/pages'; import { GuPage } from '../fixtures/pages/Page'; const { path } = articles[0] as unknown as GuPage; - testAtBreakpoints(['mobile']).forEach(({ breakpoint, width, height }) => { - test(`mobile sticky responds to banner:close event at ${breakpoint}`, async ({ + test(`can show and interact with SR banner at ${breakpoint}`, async ({ page, }) => { await page.setViewportSize({ width, height }); + await loadPage({ page, path, region: 'US' }); await cmpAcceptAll(page); await loadPage({ page, path, region: 'US', - queryParams: { adtest: 'mobileStickyTest' }, + queryParams: { 'force-banner': '', adtest: 'mobileStickyTest' }, }); - await page.evaluate(() => { - document.dispatchEvent(new Event('banner:close')); - }); + // Banner should be visible, ad slot should not exist + await expect( + page.locator('[name="StickyBottomBanner"] > *'), + ).toBeVisible(); + await expect(page.locator('#dfp-ad--mobile-sticky')).not.toBeAttached(); - await expect(page.locator('#dfp-ad--mobile-sticky')).toBeVisible(); + // Dismiss banner + await page.getByRole('button', { name: 'Collapse banner' }).click(); + await page.getByText('Maybe later').click(); + + // Banner hidden, ad slot should now appear + await expect( + page.locator('[name="StickyBottomBanner"]'), + ).not.toBeVisible(); + await expect(page.locator('#dfp-ad--mobile-sticky')).toBeAttached(); }); }); @@ -42,13 +52,12 @@ testAtBreakpoints(['mobile']).forEach(({ breakpoint, width, height }) => { adtest: 'mobileStickyTest', }, }); - await cmpAcceptAll(page); + await page.evaluate(() => { document.dispatchEvent(new Event('banner:none')); }); - await page.waitForTimeout(1000); - await expect(page.locator('#dfp-ad--mobile-sticky')).toBeVisible(); + await expect(page.locator('#dfp-ad--mobile-sticky')).toBeAttached(); }); }); From 7891d65753ed54834ce13ed11c7d9d9b61a382dc Mon Sep 17 00:00:00 2001 From: Demetrios Date: Mon, 12 Jan 2026 13:09:23 +0000 Subject: [PATCH 7/7] improve mobile sticky ad test description clarity --- bundle/playwright/tests/mobile-sticky-slot.spec.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/bundle/playwright/tests/mobile-sticky-slot.spec.ts b/bundle/playwright/tests/mobile-sticky-slot.spec.ts index 4638747b5..b67c41c8b 100644 --- a/bundle/playwright/tests/mobile-sticky-slot.spec.ts +++ b/bundle/playwright/tests/mobile-sticky-slot.spec.ts @@ -6,8 +6,8 @@ import { articles } from '../fixtures/pages'; import { GuPage } from '../fixtures/pages/Page'; const { path } = articles[0] as unknown as GuPage; -testAtBreakpoints(['mobile']).forEach(({ breakpoint, width, height }) => { - test(`can show and interact with SR banner at ${breakpoint}`, async ({ +testAtBreakpoints(['mobile']).forEach(({ width, height }) => { + test(`mobile sticky ad waits for StickyBottomBanner to be dismissed before loading`, async ({ page, }) => { await page.setViewportSize({ width, height }); @@ -39,8 +39,8 @@ testAtBreakpoints(['mobile']).forEach(({ breakpoint, width, height }) => { }); }); -testAtBreakpoints(['mobile']).forEach(({ breakpoint, width, height }) => { - test(`mobile sticky responds to banner:none event at ${breakpoint}`, async ({ +testAtBreakpoints(['mobile']).forEach(({ width, height }) => { + test(`mobile sticky responds to banner:none event at mobile breakpoint`, async ({ page, }) => { await page.setViewportSize({ width, height });