From 436a932fee73c0344ddb3e8033df61e667c27481 Mon Sep 17 00:00:00 2001 From: Christian Date: Tue, 21 Jul 2026 21:08:29 -0500 Subject: [PATCH 1/3] Detect GPT initial load configuration --- .../src/integrations/gpt.rs | 15 +++-- .../src/integrations/gpt_bootstrap.js | 32 ++++++++--- .../trusted-server-js/lib/src/core/types.ts | 11 ++-- .../lib/src/integrations/gpt/index.ts | 50 ++++++++++++----- .../lib/test/integrations/gpt/ad_init.test.ts | 56 +++++++++++++++++++ 5 files changed, 132 insertions(+), 32 deletions(-) diff --git a/crates/trusted-server-core/src/integrations/gpt.rs b/crates/trusted-server-core/src/integrations/gpt.rs index e21058a21..539d01610 100644 --- a/crates/trusted-server-core/src/integrations/gpt.rs +++ b/crates/trusted-server-core/src/integrations/gpt.rs @@ -1247,9 +1247,10 @@ mod tests { #[test] fn head_inserts_bootstrap_refreshes_ts_slots_when_initial_load_disabled() { - // Mirrors the bundle: when the publisher calls disableInitialLoad(), - // display() only registers a TS-defined slot, so the bootstrap must also - // refresh those slots or they render blank. + // Mirrors the bundle: when the publisher disables initial load through + // setConfig() or the legacy disableInitialLoad() method, display() only + // registers a TS-defined slot, so the bootstrap must also refresh those + // slots or they render blank. let config = test_config(); let integration = GptIntegration::new(config); let doc_state = IntegrationDocumentState::default(); @@ -1261,8 +1262,12 @@ mod tests { }; let combined = integration.head_inserts(&ctx).join(""); assert!( - combined.contains("disableInitialLoad"), - "bootstrap should wrap disableInitialLoad() to detect the disabled state" + combined.contains("gpt.setConfig"), + "bootstrap should wrap googletag.setConfig() to detect the disabled state" + ); + assert!( + combined.contains("pubads.disableInitialLoad"), + "bootstrap should wrap legacy disableInitialLoad() calls" ); assert!( combined.contains("gptInitialLoadDisabled"), diff --git a/crates/trusted-server-core/src/integrations/gpt_bootstrap.js b/crates/trusted-server-core/src/integrations/gpt_bootstrap.js index cc4c5c00c..9abf556c8 100644 --- a/crates/trusted-server-core/src/integrations/gpt_bootstrap.js +++ b/crates/trusted-server-core/src/integrations/gpt_bootstrap.js @@ -20,13 +20,29 @@ if (ts.adInit) return; // Track whether the publisher disabled GPT initial load. GPT exposes no - // getter for this, so wrap pubads().disableInitialLoad() to record it. With - // initial load disabled, display() only registers a slot and the ad request - // must come from a later refresh(); adInit() reads this to refresh its own - // freshly defined slots so they are not left blank. Pushed onto the command - // queue so it runs before the publisher's own disableInitialLoad() call. + // getter for this, so wrap both googletag.setConfig() and the legacy + // pubads().disableInitialLoad() method to record it. With initial load + // disabled, display() only registers a slot and the ad request must come from + // a later refresh(); adInit() reads this to refresh its own freshly defined + // slots so they are not left blank. Pushed onto the command queue so it runs + // before the publisher's own GPT configuration. (window.googletag = window.googletag || { cmd: [] }).cmd.push(function () { - var pubads = googletag.pubads && googletag.pubads(); + var gpt = window.googletag; + if ( + typeof gpt.setConfig === "function" && + !gpt.__tsInitialLoadConfigHooked + ) { + var originalSetConfig = gpt.setConfig.bind(gpt); + gpt.setConfig = function (config) { + if (config && config.disableInitialLoad === true) { + ts.gptInitialLoadDisabled = true; + } + return originalSetConfig(config); + }; + gpt.__tsInitialLoadConfigHooked = true; + } + + var pubads = gpt.pubads && gpt.pubads(); if ( !pubads || typeof pubads.disableInitialLoad !== "function" || @@ -34,10 +50,10 @@ ) { return; } - var original = pubads.disableInitialLoad.bind(pubads); + var originalDisableInitialLoad = pubads.disableInitialLoad.bind(pubads); pubads.disableInitialLoad = function () { ts.gptInitialLoadDisabled = true; - return original(); + return originalDisableInitialLoad(); }; pubads.__tsInitialLoadHooked = true; }); diff --git a/crates/trusted-server-js/lib/src/core/types.ts b/crates/trusted-server-js/lib/src/core/types.ts index 360e2aa49..e791355e6 100644 --- a/crates/trusted-server-js/lib/src/core/types.ts +++ b/crates/trusted-server-js/lib/src/core/types.ts @@ -114,11 +114,12 @@ export interface TsjsApi { */ adInitRefreshInProgress?: boolean; /** - * True once the publisher has called `googletag.pubads().disableInitialLoad()`. - * GPT exposes no getter for this state, so it is tracked by wrapping the - * setter. When set, `display()` only registers a slot and the ad request must - * come from a `refresh()`; adInit() uses this to refresh its own freshly - * defined slots so they are not left blank. + * True once the publisher has disabled GPT initial load through + * `googletag.setConfig()` or `googletag.pubads().disableInitialLoad()`. + * GPT exposes no getter for this state, so TS tracks both configuration APIs. + * When set, `display()` only registers a slot and the ad request must come + * from a `refresh()`; adInit() uses this to refresh its own freshly defined + * slots so they are not left blank. */ gptInitialLoadDisabled?: boolean; /** Guards SPA pushState hook installation. */ diff --git a/crates/trusted-server-js/lib/src/integrations/gpt/index.ts b/crates/trusted-server-js/lib/src/integrations/gpt/index.ts index ca4689684..8d5263b9b 100644 --- a/crates/trusted-server-js/lib/src/integrations/gpt/index.ts +++ b/crates/trusted-server-js/lib/src/integrations/gpt/index.ts @@ -109,6 +109,10 @@ interface GoogleTagPubAdsService { disableInitialLoad?(): void; } +interface GoogleTagConfig extends Record { + disableInitialLoad?: boolean; +} + interface GoogleTag { cmd: Array<() => void>; pubads(): GoogleTagPubAdsService; @@ -120,6 +124,7 @@ interface GoogleTag { destroySlots(slots?: GoogleTagSlot[]): boolean; enableServices(): void; display(elementId: string): void; + setConfig(config: GoogleTagConfig): void; _loaded_?: boolean; } @@ -409,15 +414,16 @@ function queueWinBillingBeacon(url: string): boolean { /** * Track whether the publisher disabled GPT initial load. * - * GPT exposes no getter for the initial-load-disabled flag, so wrap - * `pubads().disableInitialLoad()` to record it on `window.tsjs`. With initial - * load disabled, `display()` only registers a slot — the ad request must come - * from a later `refresh()`. adInit() reads this to refresh its own freshly - * defined slots so they are not left blank. + * GPT exposes no getter for the initial-load-disabled flag, so wrap both the + * modern `googletag.setConfig({ disableInitialLoad: true })` API and the legacy + * `pubads().disableInitialLoad()` method to record it on `window.tsjs`. With + * initial load disabled, `display()` only registers a slot — the ad request + * must come from a later `refresh()`. adInit() reads this to refresh its own + * freshly defined slots so they are not left blank. * - * Installed via the command queue so it runs before the publisher's own - * `disableInitialLoad()` call (the TS core script is injected ahead of the - * publisher's GPT setup). Idempotent per pubads service. + * Installed via the command queue so it runs before the publisher's own GPT + * configuration (the TS core script is injected ahead of the publisher's GPT + * setup). Idempotent per googletag object and pubads service. * * Only hooks an existing `googletag` stub — it never creates one. A plain module * import that does not activate the GPT integration must not touch @@ -430,16 +436,32 @@ function installInitialLoadDetector(ts: TsjsApi): void { const cmd = win.googletag?.cmd; if (!cmd) return; cmd.push(() => { - const pubads = win.googletag?.pubads?.(); + const gpt = win.googletag as + | (Partial & { __tsInitialLoadConfigHooked?: boolean }) + | undefined; + if (!gpt) return; + + if (typeof gpt.setConfig === 'function' && !gpt.__tsInitialLoadConfigHooked) { + const originalSetConfig = gpt.setConfig.bind(gpt); + gpt.setConfig = function (config: GoogleTagConfig) { + if (config?.disableInitialLoad === true) { + ts.gptInitialLoadDisabled = true; + } + return originalSetConfig(config); + }; + gpt.__tsInitialLoadConfigHooked = true; + } + + const pubads = gpt.pubads?.(); if (!pubads) return; const service = pubads as GoogleTagPubAdsService & { __tsInitialLoadHooked?: boolean }; if (typeof service.disableInitialLoad !== 'function' || service.__tsInitialLoadHooked) { return; } - const original = service.disableInitialLoad.bind(service); + const originalDisableInitialLoad = service.disableInitialLoad.bind(service); service.disableInitialLoad = function () { ts.gptInitialLoadDisabled = true; - return original(); + return originalDisableInitialLoad(); }; service.__tsInitialLoadHooked = true; }); @@ -612,9 +634,9 @@ export function installTsAdInit(): void { // Slots needing an explicit ad request via refresh(). Reused // publisher-owned slots always need one to pick up the just-applied // server-side targeting. TS-defined slots are normally fetched by the - // display() above — but when the publisher called - // pubads().disableInitialLoad(), display() only registers the slot and the - // ad request must come from refresh(). Without this, a TS-owned + // display() above — but when the publisher disabled initial load through + // setConfig() or the legacy pubads() method, display() only registers the + // slot and the ad request must come from refresh(). Without this, a TS-owned // first-impression slot renders blank on initial-load-disabled pages. Only // add them in that case; otherwise display() + refresh() would // double-request the impression. diff --git a/crates/trusted-server-js/lib/test/integrations/gpt/ad_init.test.ts b/crates/trusted-server-js/lib/test/integrations/gpt/ad_init.test.ts index 4a6368768..8f943e08c 100644 --- a/crates/trusted-server-js/lib/test/integrations/gpt/ad_init.test.ts +++ b/crates/trusted-server-js/lib/test/integrations/gpt/ad_init.test.ts @@ -243,6 +243,62 @@ describe('installTsAdInit', () => { expect(mockPubads.refresh).toHaveBeenCalledWith([mockSlot]); }); + it('refreshes TS-defined slots when setConfig disables GPT initial load', async () => { + // Modern GPT configuration uses googletag.setConfig() rather than the + // legacy pubads().disableInitialLoad() method. TS must detect both forms. + const mockSlot = { + addService: vi.fn().mockReturnThis(), + setTargeting: vi.fn().mockReturnThis(), + getSlotElementId: vi.fn().mockReturnValue('div-atf-sidebar'), + getTargeting: vi.fn().mockReturnValue([]), + }; + const mockPubads = { + enableSingleRequest: vi.fn(), + // Publisher has not defined this slot, so TS defines (owns) it. + getSlots: vi.fn().mockReturnValue([]), + addEventListener: vi.fn(), + refresh: vi.fn(), + }; + const displayMock = vi.fn(); + const setConfigMock = vi.fn(); + (window as TestWindow).googletag = { + cmd: { push: vi.fn((fn: () => void) => fn()) }, + defineSlot: vi.fn().mockReturnValue(mockSlot), + display: displayMock, + pubads: vi.fn().mockReturnValue(mockPubads), + enableServices: vi.fn(), + setConfig: setConfigMock, + }; + (window as TestWindow).tsjs = { + adSlots: [ + { + id: 'atf_sidebar_ad', + gam_unit_path: '/123/atf', + div_id: 'div-atf-sidebar', + formats: [[300, 250]], + targeting: {}, + }, + ], + bids: {}, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } as any; + + const { installTsAdInit } = await import('../../../src/integrations/gpt/index'); + installTsAdInit(); + + const config = { disableInitialLoad: true, singleRequest: true }; + ((window as TestWindow).googletag as { setConfig(value: typeof config): void }).setConfig( + config + ); + expect(setConfigMock).toHaveBeenCalledWith(config); + expect((window as TestWindow).tsjs!.gptInitialLoadDisabled).toBe(true); + + (window as TestWindow).tsjs!.adInit!(); + + expect(displayMock).toHaveBeenCalledWith('div-atf-sidebar'); + expect(mockPubads.refresh).toHaveBeenCalledWith([mockSlot]); + }); + it('sets adInitRefreshInProgress only for the duration of the internal refresh', async () => { const mockSlot = { addService: vi.fn().mockReturnThis(), From 3b02916a04cb67d24a976223aca4956544c36e2c Mon Sep 17 00:00:00 2001 From: Christian Date: Thu, 23 Jul 2026 12:33:47 -0500 Subject: [PATCH 2/3] Track effective GPT initial-load configuration --- .../src/integrations/gpt.rs | 4 + .../src/integrations/gpt_bootstrap.js | 37 +++- .../lib/src/integrations/gpt/index.ts | 41 +++-- .../lib/test/integrations/gpt/ad_init.test.ts | 158 +++++++++++++++++- 4 files changed, 215 insertions(+), 25 deletions(-) diff --git a/crates/trusted-server-core/src/integrations/gpt.rs b/crates/trusted-server-core/src/integrations/gpt.rs index 539d01610..0bd72bb6b 100644 --- a/crates/trusted-server-core/src/integrations/gpt.rs +++ b/crates/trusted-server-core/src/integrations/gpt.rs @@ -1265,6 +1265,10 @@ mod tests { combined.contains("gpt.setConfig"), "bootstrap should wrap googletag.setConfig() to detect the disabled state" ); + assert!( + combined.contains("gpt.getConfig"), + "bootstrap should read GPT's modern initial-load configuration" + ); assert!( combined.contains("pubads.disableInitialLoad"), "bootstrap should wrap legacy disableInitialLoad() calls" diff --git a/crates/trusted-server-core/src/integrations/gpt_bootstrap.js b/crates/trusted-server-core/src/integrations/gpt_bootstrap.js index 9abf556c8..1ea331793 100644 --- a/crates/trusted-server-core/src/integrations/gpt_bootstrap.js +++ b/crates/trusted-server-core/src/integrations/gpt_bootstrap.js @@ -19,25 +19,42 @@ var ts = (window.tsjs = window.tsjs || {}); if (ts.adInit) return; - // Track whether the publisher disabled GPT initial load. GPT exposes no - // getter for this, so wrap both googletag.setConfig() and the legacy - // pubads().disableInitialLoad() method to record it. With initial load + // Track whether the publisher disabled GPT initial load. Read the modern + // googletag.getConfig() value when available, and wrap googletag.setConfig() + // and the legacy pubads().disableInitialLoad() method as fallbacks because + // getConfig() may not report the legacy API's state. With initial load // disabled, display() only registers a slot and the ad request must come from // a later refresh(); adInit() reads this to refresh its own freshly defined // slots so they are not left blank. Pushed onto the command queue so it runs // before the publisher's own GPT configuration. + function syncInitialLoadDisabled(gpt) { + if (typeof gpt.getConfig !== "function") return false; + var config = gpt.getConfig("disableInitialLoad"); + if (!config || typeof config.disableInitialLoad === "undefined") { + return false; + } + ts.gptInitialLoadDisabled = config.disableInitialLoad === true; + return true; + } + (window.googletag = window.googletag || { cmd: [] }).cmd.push(function () { var gpt = window.googletag; + syncInitialLoadDisabled(gpt); if ( typeof gpt.setConfig === "function" && !gpt.__tsInitialLoadConfigHooked ) { var originalSetConfig = gpt.setConfig.bind(gpt); gpt.setConfig = function (config) { - if (config && config.disableInitialLoad === true) { - ts.gptInitialLoadDisabled = true; + var result = originalSetConfig.apply(gpt, arguments); + if ( + !syncInitialLoadDisabled(gpt) && + config && + "disableInitialLoad" in config + ) { + ts.gptInitialLoadDisabled = config.disableInitialLoad === true; } - return originalSetConfig(config); + return result; }; gpt.__tsInitialLoadConfigHooked = true; } @@ -52,8 +69,11 @@ } var originalDisableInitialLoad = pubads.disableInitialLoad.bind(pubads); pubads.disableInitialLoad = function () { - ts.gptInitialLoadDisabled = true; - return originalDisableInitialLoad(); + var result = originalDisableInitialLoad.apply(pubads, arguments); + if (!syncInitialLoadDisabled(gpt)) { + ts.gptInitialLoadDisabled = true; + } + return result; }; pubads.__tsInitialLoadHooked = true; }); @@ -166,6 +186,7 @@ // unless the publisher disabled initial load, in which case display() only // registers them and refresh() must request the ad — otherwise they render // blank. Only add them in that case to avoid double-requesting. + syncInitialLoadDisabled(window.googletag); var slotsNeedingRefresh = ts.gptInitialLoadDisabled ? slotsToRefresh.concat(newSlots) : slotsToRefresh; diff --git a/crates/trusted-server-js/lib/src/integrations/gpt/index.ts b/crates/trusted-server-js/lib/src/integrations/gpt/index.ts index 8d5263b9b..c82e16f68 100644 --- a/crates/trusted-server-js/lib/src/integrations/gpt/index.ts +++ b/crates/trusted-server-js/lib/src/integrations/gpt/index.ts @@ -110,7 +110,7 @@ interface GoogleTagPubAdsService { } interface GoogleTagConfig extends Record { - disableInitialLoad?: boolean; + disableInitialLoad?: boolean | null; } interface GoogleTag { @@ -124,7 +124,8 @@ interface GoogleTag { destroySlots(slots?: GoogleTagSlot[]): boolean; enableServices(): void; display(elementId: string): void; - setConfig(config: GoogleTagConfig): void; + setConfig?(config: GoogleTagConfig): void; + getConfig?(key: 'disableInitialLoad'): GoogleTagConfig | undefined; _loaded_?: boolean; } @@ -414,9 +415,9 @@ function queueWinBillingBeacon(url: string): boolean { /** * Track whether the publisher disabled GPT initial load. * - * GPT exposes no getter for the initial-load-disabled flag, so wrap both the - * modern `googletag.setConfig({ disableInitialLoad: true })` API and the legacy - * `pubads().disableInitialLoad()` method to record it on `window.tsjs`. With + * GPT's modern `getConfig()` getter may not report state set through the legacy + * `pubads().disableInitialLoad()` API, so read it when available and wrap both + * configuration APIs to record the state on `window.tsjs`. With * initial load disabled, `display()` only registers a slot — the ad request * must come from a later `refresh()`. adInit() reads this to refresh its own * freshly defined slots so they are not left blank. @@ -431,6 +432,16 @@ function queueWinBillingBeacon(url: string): boolean { * `installTsAdInit` runs, so the detector is still queued ahead of the * publisher's GPT setup. */ +function syncInitialLoadDisabled(gpt: Partial, ts: TsjsApi): boolean { + if (typeof gpt.getConfig !== 'function') return false; + + const config = gpt.getConfig('disableInitialLoad'); + if (!config || config.disableInitialLoad === undefined) return false; + + ts.gptInitialLoadDisabled = config.disableInitialLoad === true; + return true; +} + function installInitialLoadDetector(ts: TsjsApi): void { const win = window as GptWindow; const cmd = win.googletag?.cmd; @@ -441,13 +452,17 @@ function installInitialLoadDetector(ts: TsjsApi): void { | undefined; if (!gpt) return; + syncInitialLoadDisabled(gpt, ts); + if (typeof gpt.setConfig === 'function' && !gpt.__tsInitialLoadConfigHooked) { const originalSetConfig = gpt.setConfig.bind(gpt); - gpt.setConfig = function (config: GoogleTagConfig) { - if (config?.disableInitialLoad === true) { - ts.gptInitialLoadDisabled = true; + gpt.setConfig = function (...args: Parameters) { + const config = args[0]; + const result = originalSetConfig(...args); + if (!syncInitialLoadDisabled(gpt, ts) && config && 'disableInitialLoad' in config) { + ts.gptInitialLoadDisabled = config.disableInitialLoad === true; } - return originalSetConfig(config); + return result; }; gpt.__tsInitialLoadConfigHooked = true; } @@ -460,8 +475,11 @@ function installInitialLoadDetector(ts: TsjsApi): void { } const originalDisableInitialLoad = service.disableInitialLoad.bind(service); service.disableInitialLoad = function () { - ts.gptInitialLoadDisabled = true; - return originalDisableInitialLoad(); + const result = originalDisableInitialLoad(); + if (!syncInitialLoadDisabled(gpt, ts)) { + ts.gptInitialLoadDisabled = true; + } + return result; }; service.__tsInitialLoadHooked = true; }); @@ -640,6 +658,7 @@ export function installTsAdInit(): void { // first-impression slot renders blank on initial-load-disabled pages. Only // add them in that case; otherwise display() + refresh() would // double-request the impression. + syncInitialLoadDisabled(g, ts); const slotsNeedingRefresh = ts.gptInitialLoadDisabled ? slotsToRefresh.concat(newSlots) : slotsToRefresh; diff --git a/crates/trusted-server-js/lib/test/integrations/gpt/ad_init.test.ts b/crates/trusted-server-js/lib/test/integrations/gpt/ad_init.test.ts index 8f943e08c..a21d82297 100644 --- a/crates/trusted-server-js/lib/test/integrations/gpt/ad_init.test.ts +++ b/crates/trusted-server-js/lib/test/integrations/gpt/ad_init.test.ts @@ -1,3 +1,6 @@ +import { readFile } from 'node:fs/promises'; +import path from 'node:path'; + import { describe, it, expect, vi, beforeEach, afterEach, afterAll } from 'vitest'; // Track every 'message' EventListener added to window across the entire test @@ -205,6 +208,7 @@ describe('installTsAdInit', () => { refresh: vi.fn(), disableInitialLoad: vi.fn(), }; + const getConfigMock = vi.fn().mockReturnValue(undefined); const displayMock = vi.fn(); (window as TestWindow).googletag = { cmd: { push: vi.fn((fn: () => void) => fn()) }, @@ -212,6 +216,8 @@ describe('installTsAdInit', () => { display: displayMock, pubads: vi.fn().mockReturnValue(mockPubads), enableServices: vi.fn(), + // GPT's modern getter does not report legacy disableInitialLoad() state. + getConfig: getConfigMock, }; (window as TestWindow).tsjs = { adSlots: [ @@ -243,7 +249,65 @@ describe('installTsAdInit', () => { expect(mockPubads.refresh).toHaveBeenCalledWith([mockSlot]); }); - it('refreshes TS-defined slots when setConfig disables GPT initial load', async () => { + it('keeps the legacy disabled state in the edge bootstrap when getConfig is unavailable', async () => { + const mockSlot = { + addService: vi.fn().mockReturnThis(), + setTargeting: vi.fn().mockReturnThis(), + getSlotElementId: vi.fn().mockReturnValue('div-atf-sidebar'), + }; + const disableInitialLoadMock = vi.fn(); + const mockPubads = { + enableSingleRequest: vi.fn(), + getSlots: vi.fn().mockReturnValue([]), + refresh: vi.fn(), + disableInitialLoad: disableInitialLoadMock, + }; + const displayMock = vi.fn(); + const getConfigMock = vi.fn().mockReturnValue(undefined); + const googletag = { + cmd: { push: vi.fn((fn: () => void) => fn()) }, + defineSlot: vi.fn().mockReturnValue(mockSlot), + display: displayMock, + pubads: vi.fn().mockReturnValue(mockPubads), + enableServices: vi.fn(), + getConfig: getConfigMock, + }; + (window as TestWindow).googletag = googletag; + (window as TestWindow).tsjs = { + adSlots: [ + { + id: 'atf_sidebar_ad', + gam_unit_path: '/123/atf', + div_id: 'div-atf-sidebar', + formats: [[300, 250]], + targeting: {}, + }, + ], + bids: {}, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } as any; + + const bootstrap = await readFile( + path.resolve(process.cwd(), '../../trusted-server-core/src/integrations/gpt_bootstrap.js'), + 'utf8' + ); + const runBootstrap = new Function('window', 'googletag', bootstrap) as ( + window: Window, + googletag: object + ) => void; + runBootstrap(window, googletag); + + mockPubads.disableInitialLoad(); + expect(disableInitialLoadMock).toHaveBeenCalledOnce(); + expect((window as TestWindow).tsjs!.gptInitialLoadDisabled).toBe(true); + + (window as TestWindow).tsjs!.adInit!(); + + expect(displayMock).toHaveBeenCalledWith('div-atf-sidebar'); + expect(mockPubads.refresh).toHaveBeenCalledWith([mockSlot]); + }); + + it('tracks the effective initial-load state from setConfig', async () => { // Modern GPT configuration uses googletag.setConfig() rather than the // legacy pubads().disableInitialLoad() method. TS must detect both forms. const mockSlot = { @@ -260,13 +324,24 @@ describe('installTsAdInit', () => { refresh: vi.fn(), }; const displayMock = vi.fn(); - const setConfigMock = vi.fn(); + type InitialLoadConfig = { + disableInitialLoad?: boolean | null; + singleRequest?: boolean; + }; + let effectiveConfig: InitialLoadConfig = {}; + const setConfigMock = vi.fn((config: InitialLoadConfig) => { + if ('disableInitialLoad' in config) { + effectiveConfig = { disableInitialLoad: config.disableInitialLoad }; + } + }); + const getConfigMock = vi.fn(() => effectiveConfig); (window as TestWindow).googletag = { cmd: { push: vi.fn((fn: () => void) => fn()) }, defineSlot: vi.fn().mockReturnValue(mockSlot), display: displayMock, pubads: vi.fn().mockReturnValue(mockPubads), enableServices: vi.fn(), + getConfig: getConfigMock, setConfig: setConfigMock, }; (window as TestWindow).tsjs = { @@ -285,12 +360,83 @@ describe('installTsAdInit', () => { const { installTsAdInit } = await import('../../../src/integrations/gpt/index'); installTsAdInit(); + installTsAdInit(); + + const gpt = (window as TestWindow).googletag as { + setConfig(config: InitialLoadConfig): void; + }; + gpt.setConfig({ singleRequest: true }); + expect(setConfigMock).toHaveBeenCalledOnce(); + expect((window as TestWindow).tsjs!.gptInitialLoadDisabled).not.toBe(true); + + (window as TestWindow).tsjs!.adInit!(); + + expect(displayMock).toHaveBeenCalledWith('div-atf-sidebar'); + expect(mockPubads.refresh).not.toHaveBeenCalled(); const config = { disableInitialLoad: true, singleRequest: true }; - ((window as TestWindow).googletag as { setConfig(value: typeof config): void }).setConfig( - config - ); - expect(setConfigMock).toHaveBeenCalledWith(config); + gpt.setConfig(config); + expect(setConfigMock).toHaveBeenCalledTimes(2); + expect(setConfigMock).toHaveBeenLastCalledWith(config); + expect(getConfigMock).toHaveBeenCalledWith('disableInitialLoad'); + expect((window as TestWindow).tsjs!.gptInitialLoadDisabled).toBe(true); + + (window as TestWindow).tsjs!.adInit!(); + + expect(mockPubads.refresh).toHaveBeenCalledWith([mockSlot]); + + mockPubads.refresh.mockClear(); + gpt.setConfig({ disableInitialLoad: false }); + expect((window as TestWindow).tsjs!.gptInitialLoadDisabled).toBe(false); + gpt.setConfig({ disableInitialLoad: null }); + expect((window as TestWindow).tsjs!.gptInitialLoadDisabled).toBe(false); + + (window as TestWindow).tsjs!.adInit!(); + + expect(mockPubads.refresh).not.toHaveBeenCalled(); + }); + + it('reads initial-load configuration effective before detector installation', async () => { + const mockSlot = { + addService: vi.fn().mockReturnThis(), + setTargeting: vi.fn().mockReturnThis(), + getSlotElementId: vi.fn().mockReturnValue('div-atf-sidebar'), + getTargeting: vi.fn().mockReturnValue([]), + }; + const mockPubads = { + enableSingleRequest: vi.fn(), + getSlots: vi.fn().mockReturnValue([]), + addEventListener: vi.fn(), + refresh: vi.fn(), + }; + const displayMock = vi.fn(); + const getConfigMock = vi.fn().mockReturnValue({ disableInitialLoad: true }); + (window as TestWindow).googletag = { + cmd: { push: vi.fn((fn: () => void) => fn()) }, + defineSlot: vi.fn().mockReturnValue(mockSlot), + display: displayMock, + pubads: vi.fn().mockReturnValue(mockPubads), + enableServices: vi.fn(), + getConfig: getConfigMock, + }; + (window as TestWindow).tsjs = { + adSlots: [ + { + id: 'atf_sidebar_ad', + gam_unit_path: '/123/atf', + div_id: 'div-atf-sidebar', + formats: [[300, 250]], + targeting: {}, + }, + ], + bids: {}, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } as any; + + const { installTsAdInit } = await import('../../../src/integrations/gpt/index'); + installTsAdInit(); + + expect(getConfigMock).toHaveBeenCalledWith('disableInitialLoad'); expect((window as TestWindow).tsjs!.gptInitialLoadDisabled).toBe(true); (window as TestWindow).tsjs!.adInit!(); From b9ce68b345e36abaa6be50f021c369637edb1ff5 Mon Sep 17 00:00:00 2001 From: Christian Date: Thu, 23 Jul 2026 12:35:26 -0500 Subject: [PATCH 3/3] Document GPT initial-load getter fallback --- crates/trusted-server-js/lib/src/core/types.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/trusted-server-js/lib/src/core/types.ts b/crates/trusted-server-js/lib/src/core/types.ts index e791355e6..193f6912f 100644 --- a/crates/trusted-server-js/lib/src/core/types.ts +++ b/crates/trusted-server-js/lib/src/core/types.ts @@ -116,7 +116,8 @@ export interface TsjsApi { /** * True once the publisher has disabled GPT initial load through * `googletag.setConfig()` or `googletag.pubads().disableInitialLoad()`. - * GPT exposes no getter for this state, so TS tracks both configuration APIs. + * GPT's getter may not report state set through the legacy API, so TS tracks + * both configuration APIs. * When set, `display()` only registers a slot and the ad request must come * from a `refresh()`; adInit() uses this to refresh its own freshly defined * slots so they are not left blank.