From 001ad385c5c67b18b4de963bf1b233c57e793370 Mon Sep 17 00:00:00 2001 From: Aram Grigoryan <132480+aram356@users.noreply.github.com> Date: Fri, 24 Jul 2026 15:04:53 -0700 Subject: [PATCH 1/3] Decouple the prebid tsjs shim from the bundled Prebid.js The external bundle is now pure Prebid.js (core, consent and user ID modules, client-side bid adapters) and stamps a manifest on window.__tsjs_prebid_bundle. The shim ships as a server-served deferred tsjs module that installs the trustedServer adapter onto the window.pbjs global via public APIs only, so shim fixes deploy with the server instead of requiring an external bundle re-upload. --- .../src/integrations/prebid.rs | 6 +- .../src/integrations/registry.rs | 22 ++-- crates/trusted-server-core/src/publisher.rs | 6 +- crates/trusted-server-core/src/tsjs.rs | 11 +- crates/trusted-server-js/lib/build-all.mjs | 11 +- .../lib/build-prebid-external.mjs | 40 ++++++- .../lib/src/integrations/prebid/index.ts | 110 ++++++++++++------ .../test/integrations/prebid/index.test.ts | 98 +++++++++------- docs/guide/integrations/prebid.md | 12 +- 9 files changed, 208 insertions(+), 108 deletions(-) diff --git a/crates/trusted-server-core/src/integrations/prebid.rs b/crates/trusted-server-core/src/integrations/prebid.rs index a1b40281d..c15248fb0 100644 --- a/crates/trusted-server-core/src/integrations/prebid.rs +++ b/crates/trusted-server-core/src/integrations/prebid.rs @@ -921,7 +921,7 @@ pub fn register( .with_proxy(integration.clone()) .with_attribute_rewriter(integration.clone()) .with_head_injector(integration) - .without_js() + .with_deferred_js() .build(), )) } @@ -2937,8 +2937,8 @@ passphrase = "test-secret-key-32-bytes-minimum" "External prebid bundle route should be injected" ); assert!( - !processed.contains("tsjs-prebid.min.js"), - "Embedded deferred prebid bundle should not be injected" + processed.contains("tsjs-prebid.min.js"), + "Deferred tsjs prebid shim should be injected" ); } diff --git a/crates/trusted-server-core/src/integrations/registry.rs b/crates/trusted-server-core/src/integrations/registry.rs index 6f1b4dcfd..23e83d1de 100644 --- a/crates/trusted-server-core/src/integrations/registry.rs +++ b/crates/trusted-server-core/src/integrations/registry.rs @@ -1949,7 +1949,7 @@ mod tests { } #[test] - fn js_module_ids_exclude_prebid_and_include_core_js_only_modules() { + fn js_module_ids_defer_prebid_and_include_core_js_only_modules() { let settings = crate::test_support::tests::create_test_settings(); let mut settings_with_prebid = settings; settings_with_prebid @@ -1975,8 +1975,8 @@ mod tests { let deferred = registry.js_module_ids_deferred(); assert!( - !all.contains(&"prebid"), - "should not include prebid in embedded TSJS module IDs" + all.contains(&"prebid"), + "should include the prebid shim in embedded TSJS module IDs" ); assert!( immediate.contains(&"creative"), @@ -1991,8 +1991,8 @@ mod tests { "should not include prebid in immediate IDs" ); assert!( - !deferred.contains(&"prebid"), - "should not include prebid in deferred IDs" + deferred.contains(&"prebid"), + "should serve the prebid shim as a deferred module" ); } @@ -2077,7 +2077,7 @@ mod tests { } #[test] - fn js_module_ids_exclude_prebid_when_external_bundle_is_configured() { + fn js_module_ids_defer_prebid_shim_when_external_bundle_is_configured() { let mut settings = crate::test_support::tests::create_test_settings(); settings .integrations @@ -2094,16 +2094,16 @@ mod tests { let registry = IntegrationRegistry::new(&settings).expect("should create registry"); assert!( - !registry.js_module_ids().contains(&"prebid"), - "external bundle mode should not include prebid in embedded TSJS modules" + registry.js_module_ids().contains(&"prebid"), + "external bundle mode should include the prebid shim in embedded TSJS modules" ); assert!( !registry.js_module_ids_immediate().contains(&"prebid"), - "external bundle mode should not include prebid in immediate TSJS modules" + "the prebid shim should not load in the immediate TSJS bundle" ); assert!( - !registry.js_module_ids_deferred().contains(&"prebid"), - "external bundle mode should not include prebid in deferred TSJS modules" + registry.js_module_ids_deferred().contains(&"prebid"), + "the prebid shim should load as a deferred TSJS module" ); assert!( registry.has_route(&Method::GET, "/integrations/prebid/bundle.js"), diff --git a/crates/trusted-server-core/src/publisher.rs b/crates/trusted-server-core/src/publisher.rs index 4d6e9d2ee..32c4b37ab 100644 --- a/crates/trusted-server-core/src/publisher.rs +++ b/crates/trusted-server-core/src/publisher.rs @@ -3796,7 +3796,7 @@ mod tests { } #[test] - fn tsjs_dynamic_does_not_serve_embedded_prebid() { + fn tsjs_dynamic_serves_prebid_shim_when_enabled() { let settings = create_test_settings(); let registry = IntegrationRegistry::new(&settings).expect("should create integration registry"); @@ -3808,8 +3808,8 @@ mod tests { let response = handle_tsjs_dynamic(&req, ®istry).expect("should handle tsjs request"); assert_eq!( response.status(), - StatusCode::NOT_FOUND, - "should not serve embedded prebid module" + StatusCode::OK, + "should serve the deferred prebid shim module when prebid is enabled" ); } diff --git a/crates/trusted-server-core/src/tsjs.rs b/crates/trusted-server-core/src/tsjs.rs index 45aee02eb..a7b4cc2ef 100644 --- a/crates/trusted-server-core/src/tsjs.rs +++ b/crates/trusted-server-core/src/tsjs.rs @@ -192,12 +192,13 @@ mod tests { } #[test] - fn tsjs_deferred_script_src_uses_empty_hash_for_external_or_unknown_module() { - assert_eq!( - tsjs_deferred_script_src("prebid"), - "/static/tsjs=tsjs-prebid.min.js?v=", - "prebid now ships as an external bundle and has no local hash" + fn tsjs_deferred_script_src_hashes_prebid_shim_and_empties_unknown_module() { + let prebid_src = tsjs_deferred_script_src("prebid"); + assert!( + prebid_src.starts_with("/static/tsjs=tsjs-prebid.min.js?v="), + "prebid shim should be served from the deferred tsjs route" ); + assert_sha256_hex_hash(hash_query_value(&prebid_src)); assert_eq!( tsjs_deferred_script_src("unknown-module"), "/static/tsjs=tsjs-unknown-module.min.js?v=", diff --git a/crates/trusted-server-js/lib/build-all.mjs b/crates/trusted-server-js/lib/build-all.mjs index df261bd4f..2bfee01b1 100644 --- a/crates/trusted-server-js/lib/build-all.mjs +++ b/crates/trusted-server-js/lib/build-all.mjs @@ -8,9 +8,10 @@ * tsjs-core.js — core API (always included) * tsjs-.js — one per discovered integration * - * Prebid is intentionally excluded from this embedded build. Use - * build-prebid-external.mjs to generate publisher-specific Prebid bundles - * outside the Cargo build. + * The prebid integration builds here as the tsjs shim only — Prebid.js itself + * is never bundled into tsjs. Use build-prebid-external.mjs to generate the + * pure Prebid.js external bundle (core + adapters + user ID modules) that the + * shim requires at runtime via integrations.prebid.external_bundle_url. */ import fs from 'node:fs'; @@ -34,9 +35,7 @@ const integrationModules = fs.existsSync(integrationsDir) .filter((name) => { const fullPath = path.join(integrationsDir, name); return ( - name !== 'prebid' && - fs.statSync(fullPath).isDirectory() && - fs.existsSync(path.join(fullPath, 'index.ts')) + fs.statSync(fullPath).isDirectory() && fs.existsSync(path.join(fullPath, 'index.ts')) ); }) .sort() diff --git a/crates/trusted-server-js/lib/build-prebid-external.mjs b/crates/trusted-server-js/lib/build-prebid-external.mjs index 4e89723ed..8c343065c 100644 --- a/crates/trusted-server-js/lib/build-prebid-external.mjs +++ b/crates/trusted-server-js/lib/build-prebid-external.mjs @@ -182,9 +182,39 @@ function createTemporaryModulePaths() { temporaryDir, adaptersFile: path.join(temporaryDir, '_adapters.generated.ts'), userIdsFile: path.join(temporaryDir, '_user_ids.generated.ts'), + entryFile: path.join(temporaryDir, '_external_entry.generated.ts'), }; } +function generateExternalEntry(entryFile, adapters) { + const content = [ + '// Auto-generated by build-prebid-external.mjs.', + '//', + '// Pure Prebid.js external bundle: core, consent modules, user ID modules,', + '// and client-side bid adapters. The Trusted Server prebid shim', + '// (tsjs-prebid, served by the server) installs the trustedServer adapter', + '// onto the `window.pbjs` global this bundle populates and drives queue', + '// processing — this bundle intentionally does NOT call processQueue().', + "import 'prebid.js';", + "import 'prebid.js/modules/consentManagementTcf.js';", + "import 'prebid.js/modules/consentManagementGpp.js';", + "import 'prebid.js/modules/consentManagementUsp.js';", + "import 'prebid.js/modules/userId.js';", + "import './_adapters.generated';", + "import { INCLUDED_PREBID_USER_ID_MODULES } from './_user_ids.generated';", + '', + '// Manifest consumed by the tsjs prebid shim to validate that every', + '// configured client_side_bidder has its adapter compiled in.', + '(window as unknown as Record).__tsjs_prebid_bundle = Object.freeze({', + ` adapters: ${JSON.stringify(adapters)},`, + ' userIdModules: INCLUDED_PREBID_USER_ID_MODULES,', + '});', + '', + ].join('\n'); + + fs.writeFileSync(entryFile, content); +} + export function deriveBundleMetadata(bundleBytes) { const sha256 = crypto.createHash('sha256').update(bundleBytes).digest('hex'); const sri = `sha384-${crypto.createHash('sha384').update(bundleBytes).digest('base64')}`; @@ -224,6 +254,13 @@ async function buildExternalBundle(outDir, generatedModules) { 'node_modules/prebid.js/dist/src/src/adapterManager.js' ), }, + { + find: 'prebid.js/src/adRendering.js', + replacement: path.resolve( + __dirname, + 'node_modules/prebid.js/dist/src/src/adRendering.js' + ), + }, ], }, build: { @@ -233,7 +270,7 @@ async function buildExternalBundle(outDir, generatedModules) { sourcemap: false, minify: 'esbuild', rollupOptions: { - input: path.join(prebidDir, 'index.ts'), + input: generatedModules.entryFile, output: { format: 'iife', dir: outDir, @@ -270,6 +307,7 @@ export async function main(argv = process.argv.slice(2)) { try { const adapters = generateAdapterImports(args.adapters, generatedModules.adaptersFile); const userIdModules = generateUserIdImports(args.userIdModules, generatedModules.userIdsFile); + generateExternalEntry(generatedModules.entryFile, adapters); const bundle = await buildExternalBundle(args.outDir, generatedModules); const manifest = { prebidVersion: prebidPackageVersion(), diff --git a/crates/trusted-server-js/lib/src/integrations/prebid/index.ts b/crates/trusted-server-js/lib/src/integrations/prebid/index.ts index 342e4038d..825dfa628 100644 --- a/crates/trusted-server-js/lib/src/integrations/prebid/index.ts +++ b/crates/trusted-server-js/lib/src/integrations/prebid/index.ts @@ -11,29 +11,55 @@ // The shim on requestBids injects "trustedServer" into every ad unit so all // bids flow through the orchestrator. -import pbjs from 'prebid.js'; -import adapterManager from 'prebid.js/src/adapterManager.js'; -import 'prebid.js/modules/consentManagementTcf.js'; -import 'prebid.js/modules/consentManagementGpp.js'; -import 'prebid.js/modules/consentManagementUsp.js'; -import 'prebid.js/modules/userId.js'; - -// Client-side bid adapters — self-register with prebid.js on import. -// The external bundle generator aliases these placeholder modules to temporary -// modules built from its --adapters and --user-id-modules options. When a bidder -// is listed in `client_side_bidders` in trusted-server.toml, the requestBids -// shim leaves its bids untouched and the corresponding adapter handles them -// natively in the browser. -import './_adapters.generated'; - import { log } from '../../core/log'; import { buildAdRequest, parseAuctionResponse } from '../../core/auction'; import type { AuctionBid, AuctionEid } from '../../core/auction'; import type { AuctionSlot } from '../../core/types'; +import type _pbjsDefault from 'prebid.js'; -import { INCLUDED_PREBID_USER_ID_MODULES } from './_user_ids.generated'; import { PREBID_USER_ID_MODULE_REGISTRY } from './user_id_modules'; +/** + * Prebid.js public API surface (type-only; erased at build time). + * + * `getUserIdsAsEids` is added by the userId module at runtime, which the base + * package typing does not model. + */ +type PbjsGlobal = typeof _pbjsDefault & { + getUserIdsAsEids?: () => unknown[]; +}; + +// Prebid.js itself is NOT bundled into this module. It is served as the +// external bundle configured via `integrations.prebid.external_bundle_url` +// (required whenever the prebid integration is enabled) and owns the +// `window.pbjs` global. The Rust head injector emits a stub +// (`window.pbjs = window.pbjs || {que:[],cmd:[]}`) before any script runs and +// Prebid.js installs its API onto that same object, so capturing the reference +// at module scope is safe regardless of evaluation order. +const pbjs: PbjsGlobal = ( + typeof window !== 'undefined' + ? // eslint-disable-next-line @typescript-eslint/no-explicit-any + ((window as any).pbjs ??= { que: [], cmd: [] }) + : { que: [], cmd: [] } +) as PbjsGlobal; + +/** + * Manifest stamped on `window.__tsjs_prebid_bundle` by the external Prebid.js + * bundle (see build-prebid-external.mjs): which client-side bid adapters and + * user ID modules were compiled into it. + */ +interface ExternalPrebidBundleManifest { + adapters?: string[]; + userIdModules?: string[]; +} + +function getExternalBundleManifest(): ExternalPrebidBundleManifest | undefined { + if (typeof window === 'undefined') { + return undefined; + } + return (window as { __tsjs_prebid_bundle?: ExternalPrebidBundleManifest }).__tsjs_prebid_bundle; +} + const ADAPTER_CODE = 'trustedServer'; // OpenRTB permits vendor-specific agent types; PAIR uses 571187. // Keep this range aligned with the signed 32-bit Rust/OpenRTB representation. @@ -139,10 +165,11 @@ function readConfiguredUserIdNames(): string[] { } function recordUserIdModuleDiagnostics(): PrebidUserIdDiagnostics { + const includedUserIdModules = getExternalBundleManifest()?.userIdModules ?? []; const configuredUserIdNames = [...new Set(readConfiguredUserIdNames())].sort(); const coveredConfigNames = new Set( PREBID_USER_ID_MODULE_REGISTRY.filter((entry) => - INCLUDED_PREBID_USER_ID_MODULES.includes(entry.moduleName) + includedUserIdModules.includes(entry.moduleName) ).flatMap((entry) => entry.configNames) ); const missingConfiguredUserIdNames = configuredUserIdNames.filter( @@ -150,7 +177,7 @@ function recordUserIdModuleDiagnostics(): PrebidUserIdDiagnostics { ); const diagnostics: PrebidUserIdDiagnostics = { - includedModules: [...INCLUDED_PREBID_USER_ID_MODULES], + includedModules: [...includedUserIdModules], configuredUserIdNames, missingConfiguredUserIdNames, }; @@ -502,6 +529,18 @@ function collectAuctionEids(): AuctionEid[] | undefined { * 2. `config` argument — explicit overrides from the publisher's JS */ export function installPrebidNpm(config?: Partial): typeof pbjs { + // The prebid integration requires the external Prebid.js bundle + // (integrations.prebid.external_bundle_url). When it failed to load (network + // error, SRI mismatch) window.pbjs is still the head-injected stub with no + // API — installing the adapter is impossible, so bail out loudly. + if (typeof (pbjs as { registerBidAdapter?: unknown }).registerBidAdapter !== 'function') { + log.error( + '[tsjs-prebid] window.pbjs has no Prebid.js API — the external Prebid bundle ' + + 'failed to load. Prebid integration disabled.' + ); + return pbjs; + } + const injected = getInjectedConfig(); const merged: PrebidNpmConfig = { endpoint: config?.endpoint, @@ -661,7 +700,7 @@ export function installPrebidNpm(config?: Partial): typeof pbjs opts.bidsBackHandler = function (...args: unknown[]) { syncPrebidEidsCookie(); if (typeof originalBidsBack === 'function') { - originalBidsBack.apply(this, args); + (originalBidsBack as (...handlerArgs: unknown[]) => void).apply(this, args); } }; @@ -682,24 +721,27 @@ export function installPrebidNpm(config?: Partial): typeof pbjs pbjs.processQueue(); recordUserIdModuleDiagnostics(); - // Validate that every client-side bidder has its adapter registered. - // Adapters self-register on import, so a missing adapter means the bidder - // was listed in client_side_bidders but not included in the generated - // external Prebid bundle. Without the adapter the bidder is silently dropped - // from both server-side and client-side auctions. - for (const bidder of clientSideBidders) { - try { - if (!adapterManager.getBidAdapter(bidder)) { + // Validate that every client-side bidder has its adapter compiled into the + // external Prebid.js bundle. The bundle stamps its adapter list on + // window.__tsjs_prebid_bundle; a missing adapter means the bidder was listed + // in client_side_bidders but not included in the generated bundle, so it is + // silently dropped from both server-side and client-side auctions. + const bundledAdapters = getExternalBundleManifest()?.adapters; + if (bundledAdapters === undefined) { + if (clientSideBidders.size > 0) { + log.warn( + '[tsjs-prebid] external Prebid bundle did not stamp an adapter manifest; ' + + 'cannot verify client_side_bidders adapters' + ); + } + } else { + for (const bidder of clientSideBidders) { + if (!bundledAdapters.includes(bidder)) { log.error( - `[tsjs-prebid] client-side bidder "${bidder}" has no adapter loaded. ` + - `Add it to build-prebid-external.mjs --adapters.` + `[tsjs-prebid] client-side bidder "${bidder}" has no adapter in the external ` + + `Prebid bundle. Add it to build-prebid-external.mjs --adapters.` ); } - } catch { - log.error( - `[tsjs-prebid] client-side bidder "${bidder}" has no adapter loaded. ` + - `Add it to build-prebid-external.mjs --adapters.` - ); } } diff --git a/crates/trusted-server-js/lib/test/integrations/prebid/index.test.ts b/crates/trusted-server-js/lib/test/integrations/prebid/index.test.ts index 726f40b49..8827ddfea 100644 --- a/crates/trusted-server-js/lib/test/integrations/prebid/index.test.ts +++ b/crates/trusted-server-js/lib/test/integrations/prebid/index.test.ts @@ -1,6 +1,19 @@ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; -// Define mocks using vi.hoisted so they're available inside vi.mock factories +/** + * Default external-bundle manifest for tests. Mirrors what the real external + * Prebid.js bundle stamps on `window.__tsjs_prebid_bundle` (see + * build-prebid-external.mjs). Individual tests override and restore it. + */ +const DEFAULT_BUNDLE_MANIFEST = { + adapters: ['rubicon', 'openx', 'exampleBrowser', 'appnexus'], + userIdModules: ['sharedIdSystem'], +}; + +// Define mocks using vi.hoisted so they exist before the module under test is +// imported. The shim reads Prebid.js from the `window.pbjs` global (owned by +// the external bundle in production), so tests install the mock there instead +// of mocking module imports. const { mockSetConfig, mockProcessQueue, @@ -9,14 +22,11 @@ const { mockGetUserIdsAsEids, mockGetConfig, mockPbjs, - mockGetBidAdapter, - mockAdapterManager, } = vi.hoisted(() => { const mockSetConfig = vi.fn(); const mockProcessQueue = vi.fn(); const mockRequestBids = vi.fn(); const mockRegisterBidAdapter = vi.fn(); - const mockGetBidAdapter = vi.fn(); const mockGetUserIdsAsEids = vi.fn( () => [] as Array<{ source: string; uids?: Array<{ id: string; atype?: number }> }> ); @@ -29,10 +39,20 @@ const { getUserIdsAsEids: mockGetUserIdsAsEids, getConfig: mockGetConfig, adUnits: [] as any[], + que: [] as Array<() => void>, + cmd: [] as Array<() => void>, }; - const mockAdapterManager = { - getBidAdapter: mockGetBidAdapter, + + // Install the mock global BEFORE the shim module evaluates — the shim + // captures `window.pbjs` at module scope. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const w = globalThis.window as any; + w.pbjs = mockPbjs; + w.__tsjs_prebid_bundle = { + adapters: ['rubicon', 'openx', 'exampleBrowser', 'appnexus'], + userIdModules: ['sharedIdSystem'], }; + return { mockSetConfig, mockProcessQueue, @@ -41,28 +61,9 @@ const { mockGetUserIdsAsEids, mockGetConfig, mockPbjs, - mockGetBidAdapter, - mockAdapterManager, }; }); -// Mock prebid.js before importing the module under test. -// The real prebid.js cannot run in jsdom, so we provide a minimal stub. -vi.mock('prebid.js', () => ({ default: mockPbjs })); -vi.mock('prebid.js/src/adapterManager.js', () => ({ default: mockAdapterManager })); - -// Side-effect imports are no-ops in tests -vi.mock('prebid.js/modules/consentManagementTcf.js', () => ({})); -vi.mock('prebid.js/modules/consentManagementGpp.js', () => ({})); -vi.mock('prebid.js/modules/consentManagementUsp.js', () => ({})); -vi.mock('prebid.js/modules/userId.js', () => ({})); - -// Mock the build-generated imports in tests. -vi.mock('../../../src/integrations/prebid/_adapters.generated', () => ({})); -vi.mock('../../../src/integrations/prebid/_user_ids.generated', () => ({ - INCLUDED_PREBID_USER_ID_MODULES: ['sharedIdSystem'], -})); - import { collectBidders, getInjectedConfig, @@ -1410,8 +1411,8 @@ describe('prebid/client-side bidders', () => { mockPbjs.adUnits = []; mockGetUserIdsAsEids.mockReset(); mockGetUserIdsAsEids.mockReturnValue([]); - // By default, pretend all adapters are registered - mockGetBidAdapter.mockReturnValue({}); + // By default the manifest declares all adapters compiled in. + (window as any).__tsjs_prebid_bundle = DEFAULT_BUNDLE_MANIFEST; delete (window as any).__tsjs_prebid; }); @@ -1558,21 +1559,18 @@ describe('prebid/client-side bidders', () => { expect(tsBid.params.bidderParams).toEqual({}); }); - it('logs error when a client-side bidder has no adapter loaded', () => { - // rubicon is registered, but openx is not - mockGetBidAdapter.mockImplementation((bidder: string) => - bidder === 'rubicon' ? {} : undefined - ); + it('logs error when a client-side bidder has no adapter in the external bundle', () => { + // rubicon is compiled into the external bundle, but openx is not + (window as any).__tsjs_prebid_bundle = { + ...DEFAULT_BUNDLE_MANIFEST, + adapters: ['rubicon'], + }; (window as any).__tsjs_prebid = { clientSideBidders: ['rubicon', 'openx'] }; const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); installPrebidNpm(); - // Should have been called to check both bidders - expect(mockGetBidAdapter).toHaveBeenCalledWith('rubicon'); - expect(mockGetBidAdapter).toHaveBeenCalledWith('openx'); - // Should log an error for the missing adapter. // log.error() uses styled console output: console.error('%c[tsjs]%c ...:', style, reset, ...args) // so the actual message is the 4th argument. @@ -1580,22 +1578,40 @@ describe('prebid/client-side bidders', () => { const hasOpenxError = errorCalls.some((args) => args.some( (a) => - typeof a === 'string' && a.includes('client-side bidder "openx" has no adapter loaded') + typeof a === 'string' && + a.includes('client-side bidder "openx" has no adapter in the external Prebid bundle') ) ); expect(hasOpenxError).toBe(true); - // Should NOT log an error for the registered adapter + // Should NOT log an error for the compiled-in adapter const hasRubiconError = errorCalls.some((args) => args.some((a) => typeof a === 'string' && a.includes('client-side bidder "rubicon"')) ); expect(hasRubiconError).toBe(false); errorSpy.mockRestore(); + (window as any).__tsjs_prebid_bundle = DEFAULT_BUNDLE_MANIFEST; + }); + + it('warns when the external bundle stamped no adapter manifest', () => { + delete (window as any).__tsjs_prebid_bundle; + (window as any).__tsjs_prebid = { clientSideBidders: ['rubicon'] }; + + const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); + + installPrebidNpm(); + + const hasManifestWarn = warnSpy.mock.calls.some((args) => + args.some((a) => typeof a === 'string' && a.includes('did not stamp an adapter manifest')) + ); + expect(hasManifestWarn).toBe(true); + + warnSpy.mockRestore(); + (window as any).__tsjs_prebid_bundle = DEFAULT_BUNDLE_MANIFEST; }); it('does not log errors when all client-side bidders have adapters', () => { - mockGetBidAdapter.mockReturnValue({}); (window as any).__tsjs_prebid = { clientSideBidders: ['rubicon'] }; const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); @@ -1603,7 +1619,9 @@ describe('prebid/client-side bidders', () => { installPrebidNpm(); const hasAdapterError = errorSpy.mock.calls.some((args) => - args.some((a) => typeof a === 'string' && a.includes('has no adapter loaded')) + args.some( + (a) => typeof a === 'string' && a.includes('has no adapter in the external Prebid bundle') + ) ); expect(hasAdapterError).toBe(false); diff --git a/docs/guide/integrations/prebid.md b/docs/guide/integrations/prebid.md index 4bc73c0ce..e496fdd3c 100644 --- a/docs/guide/integrations/prebid.md +++ b/docs/guide/integrations/prebid.md @@ -374,11 +374,13 @@ available modules and default preset are checked in at `--user-id-modules` to `build-prebid-external.mjs` when a publisher needs a specific subset; omit it to use the default preset. -This is deliberate: Trusted Server injects a generated Prebid.js bundle so we -can install the `trustedServer` adapter and route auctions through `/auction`, -but publishers often need different User ID submodules. Moving that selection to -the external bundle keeps publisher-specific Prebid choices out of the Trusted -Server WASM artifact while preserving a manifest and bundle hash for auditing. +This is deliberate: the external bundle is pure Prebid.js (core, consent and +User ID modules, and client-side bid adapters) while the server-served TSJS +prebid shim installs the `trustedServer` adapter onto `window.pbjs` and routes +auctions through `/auction` — but publishers often need different User ID +submodules. Moving that selection to the external bundle keeps +publisher-specific Prebid choices out of the Trusted Server WASM artifact while +preserving a manifest and bundle hash for auditing. The current preset includes common ID modules such as Yahoo ConnectID, Criteo, LiveIntent, SharedID, UID2, ID5, LiveRamp IdentityLink, PubProvidedID, and From f63f31aa74b4f7da32ac59f344f23a3008ec95b3 Mon Sep 17 00:00:00 2001 From: Aram Grigoryan <132480+aram356@users.noreply.github.com> Date: Sat, 25 Jul 2026 09:49:23 -0700 Subject: [PATCH 2/3] Order the prebid.js type import before relative imports --- crates/trusted-server-js/lib/src/integrations/prebid/index.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/trusted-server-js/lib/src/integrations/prebid/index.ts b/crates/trusted-server-js/lib/src/integrations/prebid/index.ts index 825dfa628..6e97a1aa8 100644 --- a/crates/trusted-server-js/lib/src/integrations/prebid/index.ts +++ b/crates/trusted-server-js/lib/src/integrations/prebid/index.ts @@ -11,11 +11,12 @@ // The shim on requestBids injects "trustedServer" into every ad unit so all // bids flow through the orchestrator. +import type _pbjsDefault from 'prebid.js'; + import { log } from '../../core/log'; import { buildAdRequest, parseAuctionResponse } from '../../core/auction'; import type { AuctionBid, AuctionEid } from '../../core/auction'; import type { AuctionSlot } from '../../core/types'; -import type _pbjsDefault from 'prebid.js'; import { PREBID_USER_ID_MODULE_REGISTRY } from './user_id_modules'; From 2ddd19311397e6f5c49e6f2f3a52598028741e61 Mon Sep 17 00:00:00 2001 From: Aram Grigoryan <132480+aram356@users.noreply.github.com> Date: Sat, 25 Jul 2026 17:28:46 -0700 Subject: [PATCH 3/3] Lint test files and replace explicit any casts with typed helpers Widen the lint script to cover test/**, and make every test file pass it with real types: typed window views, TestBid/TestAdUnit shapes, adapter spec and requestBids parameter types, typeof-fetch casts for fetch mocks, and signature-free spies instead of unused typed parameters. --- crates/trusted-server-js/lib/package.json | 4 +- .../lib/test/core/auction.test.ts | 11 +- .../lib/test/core/config.test.ts | 2 +- .../lib/test/core/index.test.ts | 20 +- .../lib/test/core/registry.test.ts | 2 +- .../lib/test/core/request.test.ts | 74 +++-- .../test/integrations/creative/click.test.ts | 2 +- .../integrations/creative/proxy_sign.test.ts | 2 +- .../datadome/script_guard.test.ts | 1 + .../test/integrations/didomi/index.test.ts | 6 +- .../lib/test/integrations/gpt/index.test.ts | 30 +- .../integrations/lockr/script_guard.test.ts | 1 + .../test/integrations/prebid/index.test.ts | 280 +++++++++++------- .../lib/test/shared/beacon_guard.test.ts | 7 +- 14 files changed, 279 insertions(+), 163 deletions(-) diff --git a/crates/trusted-server-js/lib/package.json b/crates/trusted-server-js/lib/package.json index 2ffed57e7..47f4e29cf 100644 --- a/crates/trusted-server-js/lib/package.json +++ b/crates/trusted-server-js/lib/package.json @@ -10,8 +10,8 @@ "dev": "vite build --watch", "test": "vitest run", "test:watch": "vitest", - "lint": "eslint \"src/**/*.{ts,tsx}\"", - "lint:fix": "eslint --fix \"src/**/*.{ts,tsx}\"", + "lint": "eslint \"src/**/*.{ts,tsx}\" \"test/**/*.{ts,tsx}\"", + "lint:fix": "eslint --fix \"src/**/*.{ts,tsx}\" \"test/**/*.{ts,tsx}\"", "format": "prettier --check \"**/*.{ts,tsx,js,json,css,md}\"", "format:write": "prettier --write \"**/*.{ts,tsx,js,json,css,md}\"" }, diff --git a/crates/trusted-server-js/lib/test/core/auction.test.ts b/crates/trusted-server-js/lib/test/core/auction.test.ts index 31e020eff..50f078d0b 100644 --- a/crates/trusted-server-js/lib/test/core/auction.test.ts +++ b/crates/trusted-server-js/lib/test/core/auction.test.ts @@ -1,4 +1,5 @@ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; + import { buildAdRequest, parseAuctionResponse, sendAuction } from '../../src/core/auction'; describe('auction/buildAdRequest', () => { @@ -247,7 +248,7 @@ describe('auction/sendAuction', () => { ], }), }; - globalThis.fetch = vi.fn().mockResolvedValue(mockResponse) as any; + globalThis.fetch = vi.fn().mockResolvedValue(mockResponse) as unknown as typeof fetch; const request = { adUnits: [ @@ -274,7 +275,9 @@ describe('auction/sendAuction', () => { }); it('returns empty array on network error', async () => { - globalThis.fetch = vi.fn().mockRejectedValue(new Error('network error')) as any; + globalThis.fetch = vi + .fn() + .mockRejectedValue(new Error('network error')) as unknown as typeof fetch; const bids = await sendAuction('/auction', { adUnits: [] }); expect(bids).toEqual([]); @@ -286,7 +289,7 @@ describe('auction/sendAuction', () => { status: 200, headers: { get: () => 'text/html' }, json: async () => ({}), - }) as any; + }) as unknown as typeof fetch; const bids = await sendAuction('/auction', { adUnits: [] }); expect(bids).toEqual([]); @@ -298,7 +301,7 @@ describe('auction/sendAuction', () => { status: 500, headers: { get: () => 'application/json' }, json: async () => ({}), - }) as any; + }) as unknown as typeof fetch; const bids = await sendAuction('/auction', { adUnits: [] }); expect(bids).toEqual([]); diff --git a/crates/trusted-server-js/lib/test/core/config.test.ts b/crates/trusted-server-js/lib/test/core/config.test.ts index 2b15d1929..f2d849320 100644 --- a/crates/trusted-server-js/lib/test/core/config.test.ts +++ b/crates/trusted-server-js/lib/test/core/config.test.ts @@ -16,7 +16,7 @@ describe('config', () => { setConfig({ debug: true }); expect(log.getLevel()).toBe('debug'); - setConfig({ logLevel: 'info' as any }); + setConfig({ logLevel: 'info' } as Parameters[0]); expect(log.getLevel()).toBe('info'); }); }); diff --git a/crates/trusted-server-js/lib/test/core/index.test.ts b/crates/trusted-server-js/lib/test/core/index.test.ts index dc77439b1..7b887474a 100644 --- a/crates/trusted-server-js/lib/test/core/index.test.ts +++ b/crates/trusted-server-js/lib/test/core/index.test.ts @@ -1,18 +1,24 @@ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; -declare global { - interface Window { - tsjs?: any; - } +interface TsjsTestWindow { + tsjs?: { + que?: Array<() => void>; + version?: string; + setConfig?: unknown; + getConfig?: unknown; + log?: unknown; + } & Record; } +const testWindow = window as unknown as TsjsTestWindow; + const ORIGINAL_FETCH = global.fetch; describe('core/index', () => { beforeEach(async () => { await vi.resetModules(); document.body.innerHTML = ''; - delete (window as any).tsjs; + delete testWindow.tsjs; }); afterEach(() => { @@ -41,7 +47,7 @@ describe('core/index', () => { }); it('preserves edge-injected adSlots and bids set before the bundle loads', async () => { - (window as any).tsjs = { + testWindow.tsjs = { adSlots: [{ id: 'pre-injected' }], bids: { 'pre-injected': { hb_pb: '1.00' } }, }; @@ -56,7 +62,7 @@ describe('core/index', () => { const callback = vi.fn(function () { expect(this).toBe(window.tsjs); }); - (window as any).tsjs = { que: [callback] }; + testWindow.tsjs = { que: [callback] }; await import('../../src/core/index'); diff --git a/crates/trusted-server-js/lib/test/core/registry.test.ts b/crates/trusted-server-js/lib/test/core/registry.test.ts index 726f67797..7190a085b 100644 --- a/crates/trusted-server-js/lib/test/core/registry.test.ts +++ b/crates/trusted-server-js/lib/test/core/registry.test.ts @@ -17,7 +17,7 @@ describe('registry', () => { ], }, }, - } as any; + } as unknown as Parameters[0]; addAdUnits(unit); const all = getAllUnits(); diff --git a/crates/trusted-server-js/lib/test/core/request.test.ts b/crates/trusted-server-js/lib/test/core/request.test.ts index 2c56361dc..efc18948f 100644 --- a/crates/trusted-server-js/lib/test/core/request.test.ts +++ b/crates/trusted-server-js/lib/test/core/request.test.ts @@ -1,5 +1,10 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +/** Test view of the global scope with a mockable `fetch`. */ +const testGlobal = globalThis as unknown as { fetch: ReturnType }; + +type AddAdUnitsArg = Parameters[0]; + async function flushRequestAds(): Promise { await new Promise((resolve) => setTimeout(resolve, 0)); } @@ -21,7 +26,7 @@ describe('request.requestAds', () => { it('sends fetch and renders creatives via iframe from response', async () => { // mock fetch - returns creative HTML inline in adm field const creativeHtml = '
Test Creative
'; - (globalThis as any).fetch = vi.fn().mockResolvedValue({ + testGlobal.fetch = vi.fn().mockResolvedValue({ ok: true, status: 200, headers: { get: () => 'application/json' }, @@ -41,12 +46,15 @@ describe('request.requestAds', () => { const infoSpy = vi.spyOn(log, 'info').mockImplementation(() => undefined); document.body.innerHTML = '
'; - addAdUnits({ code: 'slot1', mediaTypes: { banner: { sizes: [[300, 250]] } } } as any); + addAdUnits({ + code: 'slot1', + mediaTypes: { banner: { sizes: [[300, 250]] } }, + } as unknown as AddAdUnitsArg); requestAds(); await flushRequestAds(); - expect((globalThis as any).fetch).toHaveBeenCalled(); + expect(testGlobal.fetch).toHaveBeenCalled(); // Verify iframe was created with creative HTML in srcdoc const iframe = document.querySelector('#slot1 iframe') as HTMLIFrameElement | null; @@ -67,7 +75,7 @@ describe('request.requestAds', () => { }); it('does not render on non-JSON response', async () => { - (globalThis as any).fetch = vi.fn().mockResolvedValue({ + testGlobal.fetch = vi.fn().mockResolvedValue({ ok: true, status: 200, headers: { get: () => 'text/plain' }, @@ -78,35 +86,41 @@ describe('request.requestAds', () => { const { requestAds } = await import('../../src/core/request'); document.body.innerHTML = '
'; - addAdUnits({ code: 'slot1', mediaTypes: { banner: { sizes: [[300, 250]] } } } as any); + addAdUnits({ + code: 'slot1', + mediaTypes: { banner: { sizes: [[300, 250]] } }, + } as unknown as AddAdUnitsArg); requestAds(); await flushRequestAds(); - expect((globalThis as any).fetch).toHaveBeenCalled(); + expect(testGlobal.fetch).toHaveBeenCalled(); expect(document.querySelector('iframe')).toBeNull(); }); it('ignores fetch rejection gracefully', async () => { - (globalThis as any).fetch = vi.fn().mockRejectedValue(new Error('network-error')); + testGlobal.fetch = vi.fn().mockRejectedValue(new Error('network-error')); const { addAdUnits } = await import('../../src/core/registry'); const { requestAds } = await import('../../src/core/request'); document.body.innerHTML = '
'; - addAdUnits({ code: 'slot1', mediaTypes: { banner: { sizes: [[300, 250]] } } } as any); + addAdUnits({ + code: 'slot1', + mediaTypes: { banner: { sizes: [[300, 250]] } }, + } as unknown as AddAdUnitsArg); requestAds(); await flushRequestAds(); - expect((globalThis as any).fetch).toHaveBeenCalled(); + expect(testGlobal.fetch).toHaveBeenCalled(); expect(document.querySelector('iframe')).toBeNull(); }); it('inserts an iframe with creative HTML from unified auction', async () => { // mock fetch for unified auction endpoint - returns inline HTML const creativeHtml = 'Ad'; - (globalThis as any).fetch = vi.fn().mockResolvedValue({ + testGlobal.fetch = vi.fn().mockResolvedValue({ ok: true, status: 200, headers: { get: () => 'application/json' }, @@ -129,7 +143,10 @@ describe('request.requestAds', () => { document.body.appendChild(div); // Add an ad unit and request - addAdUnits({ code: 'slot1', mediaTypes: { banner: { sizes: [[300, 250]] } } } as any); + addAdUnits({ + code: 'slot1', + mediaTypes: { banner: { sizes: [[300, 250]] } }, + } as unknown as AddAdUnitsArg); requestAds(); await flushRequestAds(); @@ -144,7 +161,7 @@ describe('request.requestAds', () => { it('renders creatives with safe URI markup', async () => { const creativeHtml = 'Contactad'; - (globalThis as any).fetch = vi.fn().mockResolvedValue({ + testGlobal.fetch = vi.fn().mockResolvedValue({ ok: true, status: 200, headers: { get: () => 'application/json' }, @@ -162,7 +179,10 @@ describe('request.requestAds', () => { const { requestAds } = await import('../../src/core/request'); document.body.innerHTML = '
'; - addAdUnits({ code: 'slot1', mediaTypes: { banner: { sizes: [[300, 250]] } } } as any); + addAdUnits({ + code: 'slot1', + mediaTypes: { banner: { sizes: [[300, 250]] } }, + } as unknown as AddAdUnitsArg); requestAds(); await flushRequestAds(); @@ -174,7 +194,7 @@ describe('request.requestAds', () => { }); it('rejects malformed non-string creative HTML without blanking the slot', async () => { - (globalThis as any).fetch = vi.fn().mockResolvedValue({ + testGlobal.fetch = vi.fn().mockResolvedValue({ ok: true, status: 200, headers: { get: () => 'application/json' }, @@ -194,7 +214,10 @@ describe('request.requestAds', () => { const warnSpy = vi.spyOn(log, 'warn').mockImplementation(() => undefined); document.body.innerHTML = '
existing
'; - addAdUnits({ code: 'slot1', mediaTypes: { banner: { sizes: [[300, 250]] } } } as any); + addAdUnits({ + code: 'slot1', + mediaTypes: { banner: { sizes: [[300, 250]] } }, + } as unknown as AddAdUnitsArg); requestAds(); await flushRequestAds(); @@ -221,7 +244,7 @@ describe('request.requestAds', () => { // Regression: multi-bid scenario where a rejected bid must not erase an earlier // successful render into the same slot. const goodCreative = '
Safe Ad
'; - (globalThis as any).fetch = vi.fn().mockResolvedValue({ + testGlobal.fetch = vi.fn().mockResolvedValue({ ok: true, status: 200, headers: { get: () => 'application/json' }, @@ -244,7 +267,10 @@ describe('request.requestAds', () => { const { requestAds } = await import('../../src/core/request'); document.body.innerHTML = '
'; - addAdUnits({ code: 'slot1', mediaTypes: { banner: { sizes: [[300, 250]] } } } as any); + addAdUnits({ + code: 'slot1', + mediaTypes: { banner: { sizes: [[300, 250]] } }, + } as unknown as AddAdUnitsArg); requestAds(); await flushRequestAds(); @@ -256,7 +282,7 @@ describe('request.requestAds', () => { }); it('rejects creatives that sanitize to empty markup', async () => { - (globalThis as any).fetch = vi.fn().mockResolvedValue({ + testGlobal.fetch = vi.fn().mockResolvedValue({ ok: true, status: 200, headers: { get: () => 'application/json' }, @@ -276,7 +302,10 @@ describe('request.requestAds', () => { const warnSpy = vi.spyOn(log, 'warn').mockImplementation(() => undefined); document.body.innerHTML = '
'; - addAdUnits({ code: 'slot1', mediaTypes: { banner: { sizes: [[300, 250]] } } } as any); + addAdUnits({ + code: 'slot1', + mediaTypes: { banner: { sizes: [[300, 250]] } }, + } as unknown as AddAdUnitsArg); requestAds(); await flushRequestAds(); @@ -298,7 +327,7 @@ describe('request.requestAds', () => { it('skips iframe insertion when slot is missing', async () => { // mock fetch for unified auction endpoint - returns inline HTML - (globalThis as any).fetch = vi.fn().mockResolvedValue({ + testGlobal.fetch = vi.fn().mockResolvedValue({ ok: true, status: 200, headers: { get: () => 'application/json' }, @@ -314,7 +343,10 @@ describe('request.requestAds', () => { const { addAdUnits } = await import('../../src/core/registry'); const { requestAds } = await import('../../src/core/request'); - addAdUnits({ code: 'missing-slot', mediaTypes: { banner: { sizes: [[300, 250]] } } } as any); + addAdUnits({ + code: 'missing-slot', + mediaTypes: { banner: { sizes: [[300, 250]] } }, + } as unknown as AddAdUnitsArg); requestAds(); await flushRequestAds(); diff --git a/crates/trusted-server-js/lib/test/integrations/creative/click.test.ts b/crates/trusted-server-js/lib/test/integrations/creative/click.test.ts index 05dcd0e02..7cf31afa3 100644 --- a/crates/trusted-server-js/lib/test/integrations/creative/click.test.ts +++ b/crates/trusted-server-js/lib/test/integrations/creative/click.test.ts @@ -17,7 +17,7 @@ describe('creative/click.ts', () => { it('repairs anchors via proxy rebuild fallback when fetch is unavailable', async () => { vi.useFakeTimers(); - global.fetch = undefined as any; + global.fetch = undefined as unknown as typeof fetch; const anchor = document.createElement('a'); anchor.setAttribute('data-tsclick', FIRST_PARTY_CLICK); diff --git a/crates/trusted-server-js/lib/test/integrations/creative/proxy_sign.test.ts b/crates/trusted-server-js/lib/test/integrations/creative/proxy_sign.test.ts index 41c86a873..7f31dbed9 100644 --- a/crates/trusted-server-js/lib/test/integrations/creative/proxy_sign.test.ts +++ b/crates/trusted-server-js/lib/test/integrations/creative/proxy_sign.test.ts @@ -47,7 +47,7 @@ describe('creative/proxy_sign.ts', () => { }); it('returns null when fetch is unavailable', async () => { - global.fetch = undefined as any; + global.fetch = undefined as unknown as typeof fetch; const result = await signProxyUrl('https://cdn.example/asset.js'); expect(result).toBeNull(); }); diff --git a/crates/trusted-server-js/lib/test/integrations/datadome/script_guard.test.ts b/crates/trusted-server-js/lib/test/integrations/datadome/script_guard.test.ts index 795b442a6..70fe191fd 100644 --- a/crates/trusted-server-js/lib/test/integrations/datadome/script_guard.test.ts +++ b/crates/trusted-server-js/lib/test/integrations/datadome/script_guard.test.ts @@ -1,4 +1,5 @@ import { describe, it, expect, beforeEach, afterEach } from 'vitest'; + import { installDataDomeGuard, isGuardInstalled, diff --git a/crates/trusted-server-js/lib/test/integrations/didomi/index.test.ts b/crates/trusted-server-js/lib/test/integrations/didomi/index.test.ts index 487ff471f..bc347968c 100644 --- a/crates/trusted-server-js/lib/test/integrations/didomi/index.test.ts +++ b/crates/trusted-server-js/lib/test/integrations/didomi/index.test.ts @@ -5,7 +5,7 @@ import { installDidomiSdkProxy } from '../../../src/integrations/didomi'; const ORIGINAL_WINDOW = global.window; type TestDidomiWindow = Window & { - didomiConfig?: any; + didomiConfig?: Record; __tsjs_didomi?: { proxyPath?: string }; }; @@ -20,11 +20,11 @@ describe('integrations/didomi', () => { beforeEach(() => { testWindow = createWindow('https://example.com/page'); - Object.assign(globalThis as any, { window: testWindow }); + Object.assign(globalThis as unknown as { window: unknown }, { window: testWindow }); }); afterEach(() => { - Object.assign(globalThis as any, { window: ORIGINAL_WINDOW }); + Object.assign(globalThis as unknown as { window: unknown }, { window: ORIGINAL_WINDOW }); }); it('initializes didomiConfig and forces sdkPath through trusted server proxy', () => { diff --git a/crates/trusted-server-js/lib/test/integrations/gpt/index.test.ts b/crates/trusted-server-js/lib/test/integrations/gpt/index.test.ts index 406c6d1f5..9a3c774e1 100644 --- a/crates/trusted-server-js/lib/test/integrations/gpt/index.test.ts +++ b/crates/trusted-server-js/lib/test/integrations/gpt/index.test.ts @@ -1,5 +1,13 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +/** Window properties these tests read and write on the jsdom global. */ +interface GptTestWindow { + googletag?: unknown; + tsjs?: Record & { adInit?: () => void }; +} + +const gptTestWindow = window as unknown as GptTestWindow; + // We import installGptShim dynamically so each test can control whether the // GPT enable flag is present before module evaluation. @@ -219,14 +227,14 @@ describe('GPT – installSlimPrebidLoader', () => { describe('GPT – installTsAdInit', () => { beforeEach(() => { document.body.innerHTML = ''; - delete (window as any).tsjs; - delete (window as any).googletag; + delete gptTestWindow.tsjs; + delete gptTestWindow.googletag; }); afterEach(() => { document.body.innerHTML = ''; - delete (window as any).tsjs; - delete (window as any).googletag; + delete gptTestWindow.tsjs; + delete gptTestWindow.googletag; }); it('clears stale TS-managed targeting before applying a new route to a reused GPT slot', async () => { @@ -240,7 +248,13 @@ describe('GPT – installTsAdInit', () => { ['ts_initial', ['1']], ['pos', ['old-pos']], ]); - const gptSlot: any = { + interface GptTestSlot { + getSlotElementId: () => string; + getTargeting: (key: string) => string[]; + setTargeting: (key: string, value: string | string[]) => GptTestSlot; + clearTargeting: (key?: string) => GptTestSlot; + } + const gptSlot: GptTestSlot = { getSlotElementId: vi.fn(() => 'div-ad-homepage-header'), getTargeting: vi.fn((key: string) => slotTargeting.get(key) ?? []), setTargeting: vi.fn((key: string, value: string | string[]) => { @@ -269,14 +283,14 @@ describe('GPT – installTsAdInit', () => { }; document.body.innerHTML = '
'; - (window as any).googletag = { + gptTestWindow.googletag = { cmd, pubads: () => pubads, defineSlot: vi.fn(), destroySlots: vi.fn(), enableServices: vi.fn(), }; - (window as any).tsjs = { + gptTestWindow.tsjs = { prevSlotTargetingKeys: { 'div-ad-homepage-header': ['pos'], }, @@ -293,7 +307,7 @@ describe('GPT – installTsAdInit', () => { }; installTsAdInit(); - (window as any).tsjs.adInit(); + gptTestWindow.tsjs?.adInit?.(); expect(gptSlot.clearTargeting).toHaveBeenCalledWith('hb_pb'); expect(gptSlot.clearTargeting).toHaveBeenCalledWith('hb_bidder'); diff --git a/crates/trusted-server-js/lib/test/integrations/lockr/script_guard.test.ts b/crates/trusted-server-js/lib/test/integrations/lockr/script_guard.test.ts index b9251b1e1..2f53c06b2 100644 --- a/crates/trusted-server-js/lib/test/integrations/lockr/script_guard.test.ts +++ b/crates/trusted-server-js/lib/test/integrations/lockr/script_guard.test.ts @@ -1,4 +1,5 @@ import { describe, it, expect, beforeEach, afterEach } from 'vitest'; + import { installLockrGuard, isGuardInstalled, diff --git a/crates/trusted-server-js/lib/test/integrations/prebid/index.test.ts b/crates/trusted-server-js/lib/test/integrations/prebid/index.test.ts index 8827ddfea..665fddd42 100644 --- a/crates/trusted-server-js/lib/test/integrations/prebid/index.test.ts +++ b/crates/trusted-server-js/lib/test/integrations/prebid/index.test.ts @@ -10,6 +10,59 @@ const DEFAULT_BUNDLE_MANIFEST = { userIdModules: ['sharedIdSystem'], }; +/** Loose bid shape used by the requestBids shim tests. */ +interface TestBid { + bidder: string; + params?: Record; +} + +/** Loose ad unit shape used by the requestBids shim tests. */ +interface TestAdUnit { + code?: string; + bids?: TestBid[]; +} + +/** Window properties the prebid shim reads and writes in these tests. */ +interface PrebidTestWindow { + pbjs?: unknown; + tsjs?: unknown; + googletag?: unknown; + __tsjs_prebid?: Record; + __tsjs_prebid_bundle?: { adapters?: string[]; userIdModules?: string[] }; + __tsjs_prebid_diagnostics?: { + userIdModules?: { + includedModules: string[]; + configuredUserIdNames: string[]; + missingConfiguredUserIdNames: string[]; + }; + }; +} + +const testWindow = window as unknown as PrebidTestWindow; + +/** Argument type accepted by the shimmed `pbjs.requestBids`. */ +type RequestBidsArg = Parameters['requestBids']>[0]; + +/** The bid adapter spec object registered via `pbjs.registerBidAdapter`. */ +interface TestAdapterSpec { + code: string; + supportedMediaTypes: string[]; + isBidRequestValid: (bid: Record) => boolean; + buildRequests: ( + bidRequests: Array>, + bidderRequest?: Record + ) => { + method: string; + url: string; + data: Record; + options: Record; + }; + interpretResponse: ( + response: Record, + request?: Record + ) => Array>; +} + // Define mocks using vi.hoisted so they exist before the module under test is // imported. The shim reads Prebid.js from the `window.pbjs` global (owned by // the external bundle in production), so tests install the mock there instead @@ -38,15 +91,18 @@ const { registerBidAdapter: mockRegisterBidAdapter, getUserIdsAsEids: mockGetUserIdsAsEids, getConfig: mockGetConfig, - adUnits: [] as any[], + adUnits: [] as TestAdUnit[], + setTargetingForGPTAsync: undefined as ((adUnitCodes?: string[]) => void) | undefined, que: [] as Array<() => void>, cmd: [] as Array<() => void>, }; // Install the mock global BEFORE the shim module evaluates — the shim // captures `window.pbjs` at module scope. - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const w = globalThis.window as any; + const w = globalThis.window as unknown as { + pbjs?: unknown; + __tsjs_prebid_bundle?: unknown; + }; w.pbjs = mockPbjs; w.__tsjs_prebid_bundle = { adapters: ['rubicon', 'openx', 'exampleBrowser', 'appnexus'], @@ -103,7 +159,7 @@ describe('prebid/collectBidders', () => { describe('prebid/getInjectedConfig', () => { afterEach(() => { - delete (window as any).__tsjs_prebid; + delete testWindow.__tsjs_prebid; }); it('returns undefined when window.__tsjs_prebid is not set', () => { @@ -111,7 +167,7 @@ describe('prebid/getInjectedConfig', () => { }); it('returns the injected config when present', () => { - (window as any).__tsjs_prebid = { accountId: 'server-42', timeout: 2000 }; + testWindow.__tsjs_prebid = { accountId: 'server-42', timeout: 2000 }; expect(getInjectedConfig()).toEqual({ accountId: 'server-42', timeout: 2000 }); }); }); @@ -217,8 +273,8 @@ describe('prebid/installPrebidNpm', () => { mockGetUserIdsAsEids.mockReturnValue([]); mockGetConfig.mockReset(); document.cookie = 'ts-eids=; Path=/; Max-Age=0'; - delete (window as any).__tsjs_prebid; - delete (window as any).__tsjs_prebid_diagnostics; + delete testWindow.__tsjs_prebid; + delete testWindow.__tsjs_prebid_diagnostics; }); afterEach(() => { @@ -268,7 +324,7 @@ describe('prebid/installPrebidNpm', () => { it('reports the User ID modules selected by the generated bundle', () => { installPrebidNpm(); - expect((window as any).__tsjs_prebid_diagnostics.userIdModules).toEqual({ + expect(testWindow.__tsjs_prebid_diagnostics.userIdModules).toEqual({ includedModules: ['sharedIdSystem'], configuredUserIdNames: [], missingConfiguredUserIdNames: [], @@ -285,7 +341,7 @@ describe('prebid/installPrebidNpm', () => { mockPbjs.requestBids({ adUnits: [] }); mockPbjs.requestBids({ adUnits: [] }); - expect((window as any).__tsjs_prebid_diagnostics.userIdModules).toEqual({ + expect(testWindow.__tsjs_prebid_diagnostics.userIdModules).toEqual({ includedModules: ['sharedIdSystem'], configuredUserIdNames: ['pairId', 'sharedId'], missingConfiguredUserIdNames: ['pairId'], @@ -301,9 +357,9 @@ describe('prebid/installPrebidNpm', () => { }); describe('adapter spec', () => { - function getAdapterSpec(): any { + function getAdapterSpec(): TestAdapterSpec { installPrebidNpm(); - return mockRegisterBidAdapter.mock.calls[0][2]; + return mockRegisterBidAdapter.mock.calls[0][2] as TestAdapterSpec; } it('isBidRequestValid always returns true', () => { @@ -581,18 +637,18 @@ describe('prebid/installPrebidNpm', () => { { bids: [{ bidder: 'appnexus', params: {} }] }, { bids: [{ bidder: 'rubicon', params: {} }] }, ]; - pbjs.requestBids({ adUnits } as any); + pbjs.requestBids({ adUnits } as unknown as RequestBidsArg); // Each ad unit should have trustedServer added for (const unit of adUnits) { - const hasTsBidder = unit.bids.some((b: any) => b.bidder === 'trustedServer'); + const hasTsBidder = unit.bids.some((b: TestBid) => b.bidder === 'trustedServer'); expect(hasTsBidder).toBe(true); } - const trustedServerBid = adUnits[0].bids.find((b: any) => b.bidder === 'trustedServer'); + const trustedServerBid = adUnits[0].bids.find((b: TestBid) => b.bidder === 'trustedServer'); expect(trustedServerBid.params.bidderParams).toEqual({ appnexus: {} }); - expect(adUnits[0].bids.map((b: any) => b.bidder)).toEqual(['trustedServer']); - expect(adUnits[1].bids.map((b: any) => b.bidder)).toEqual(['trustedServer']); + expect(adUnits[0].bids.map((b: TestBid) => b.bidder)).toEqual(['trustedServer']); + expect(adUnits[1].bids.map((b: TestBid) => b.bidder)).toEqual(['trustedServer']); // Should call through to original requestBids expect(mockRequestBids).toHaveBeenCalled(); @@ -602,9 +658,9 @@ describe('prebid/installPrebidNpm', () => { const pbjs = installPrebidNpm(); const adUnits = [{ bids: [{ bidder: 'trustedServer', params: {} }] }]; - pbjs.requestBids({ adUnits } as any); + pbjs.requestBids({ adUnits } as unknown as RequestBidsArg); - const tsCount = adUnits[0].bids.filter((b: any) => b.bidder === 'trustedServer').length; + const tsCount = adUnits[0].bids.filter((b: TestBid) => b.bidder === 'trustedServer').length; expect(tsCount).toBe(1); }); @@ -619,15 +675,15 @@ describe('prebid/installPrebidNpm', () => { ], }, ]; - pbjs.requestBids({ adUnits } as any); + pbjs.requestBids({ adUnits } as unknown as RequestBidsArg); - const trustedServerBid = adUnits[0].bids.find((b: any) => b.bidder === 'trustedServer'); + const trustedServerBid = adUnits[0].bids.find((b: TestBid) => b.bidder === 'trustedServer'); expect(trustedServerBid).toBeDefined(); expect(trustedServerBid.params.bidderParams).toEqual({ appnexus: { placementId: 123 }, rubicon: { accountId: 'abc' }, }); - expect(adUnits[0].bids.map((b: any) => b.bidder)).toEqual(['trustedServer']); + expect(adUnits[0].bids.map((b: TestBid) => b.bidder)).toEqual(['trustedServer']); }); it('preserves captured bidder params when requestBids runs twice on the same ad unit', () => { @@ -643,16 +699,16 @@ describe('prebid/installPrebidNpm', () => { ], }, ]; - pbjs.requestBids({ adUnits } as any); + pbjs.requestBids({ adUnits } as unknown as RequestBidsArg); // Second auction (refresh/re-auction) with the SAME ad unit object: the // server-side bidder entries were already pruned, so the shim must not // overwrite the captured params with an empty object. - pbjs.requestBids({ adUnits } as any); + pbjs.requestBids({ adUnits } as unknown as RequestBidsArg); const trustedServerBid = adUnits[0].bids.find( - (b: any) => b.bidder === 'trustedServer' - ) as any; + (b: TestBid) => b.bidder === 'trustedServer' + ) as TestBid; expect(trustedServerBid.params.bidderParams).toEqual({ appnexus: { placementId: 123 }, rubicon: { accountId: 'abc' }, @@ -662,8 +718,8 @@ describe('prebid/installPrebidNpm', () => { it('adds bids array to ad units that have none', () => { const pbjs = installPrebidNpm(); - const adUnits = [{ code: 'div-1' }] as any[]; - pbjs.requestBids({ adUnits } as any); + const adUnits = [{ code: 'div-1' }] as TestAdUnit[]; + pbjs.requestBids({ adUnits } as unknown as RequestBidsArg); expect(adUnits[0].bids).toHaveLength(1); expect(adUnits[0].bids[0].bidder).toBe('trustedServer'); @@ -684,12 +740,12 @@ describe('prebid/installPrebidNpm', () => { bids: [{ bidder: 'kargo', params: { placementId: '_def' } }], }, ]; - pbjs.requestBids({ adUnits } as any); + pbjs.requestBids({ adUnits } as unknown as RequestBidsArg); - const tsBid0 = adUnits[0].bids.find((b: any) => b.bidder === 'trustedServer') as any; + const tsBid0 = adUnits[0].bids.find((b: TestBid) => b.bidder === 'trustedServer') as TestBid; expect(tsBid0.params.zone).toBe('header'); - const tsBid1 = adUnits[1].bids.find((b: any) => b.bidder === 'trustedServer') as any; + const tsBid1 = adUnits[1].bids.find((b: TestBid) => b.bidder === 'trustedServer') as TestBid; expect(tsBid1.params.zone).toBe('fixed_bottom'); }); @@ -703,9 +759,9 @@ describe('prebid/installPrebidNpm', () => { bids: [{ bidder: 'appnexus', params: {} }], }, ]; - pbjs.requestBids({ adUnits } as any); + pbjs.requestBids({ adUnits } as unknown as RequestBidsArg); - const tsBid = adUnits[0].bids.find((b: any) => b.bidder === 'trustedServer') as any; + const tsBid = adUnits[0].bids.find((b: TestBid) => b.bidder === 'trustedServer') as TestBid; expect(tsBid.params.zone).toBeUndefined(); }); @@ -713,9 +769,9 @@ describe('prebid/installPrebidNpm', () => { const pbjs = installPrebidNpm(); const adUnits = [{ bids: [{ bidder: 'rubicon', params: {} }] }]; - pbjs.requestBids({ adUnits } as any); + pbjs.requestBids({ adUnits } as unknown as RequestBidsArg); - const tsBid = adUnits[0].bids.find((b: any) => b.bidder === 'trustedServer') as any; + const tsBid = adUnits[0].bids.find((b: TestBid) => b.bidder === 'trustedServer') as TestBid; expect(tsBid.params.zone).toBeUndefined(); }); @@ -733,16 +789,16 @@ describe('prebid/installPrebidNpm', () => { }, ]; - pbjs.requestBids({ adUnits } as any); + pbjs.requestBids({ adUnits } as unknown as RequestBidsArg); - let tsBid = adUnits[0].bids.find((b: any) => b.bidder === 'trustedServer') as any; + let tsBid = adUnits[0].bids.find((b: TestBid) => b.bidder === 'trustedServer') as TestBid; expect(tsBid.params.zone).toBe('header'); expect(tsBid.params.custom).toBe('keep'); delete adUnits[0].mediaTypes.banner.name; - pbjs.requestBids({ adUnits } as any); + pbjs.requestBids({ adUnits } as unknown as RequestBidsArg); - tsBid = adUnits[0].bids.find((b: any) => b.bidder === 'trustedServer') as any; + tsBid = adUnits[0].bids.find((b: TestBid) => b.bidder === 'trustedServer') as TestBid; expect(tsBid.params.zone).toBeUndefined(); expect(tsBid.params.custom).toBe('keep'); }); @@ -750,11 +806,11 @@ describe('prebid/installPrebidNpm', () => { it('falls back to pbjs.adUnits when requestObj has no adUnits', () => { const pbjs = installPrebidNpm(); - mockPbjs.adUnits = [{ bids: [{ bidder: 'openx', params: {} }] }] as any[]; - pbjs.requestBids({} as any); + mockPbjs.adUnits = [{ bids: [{ bidder: 'openx', params: {} }] }] as TestAdUnit[]; + pbjs.requestBids({} as RequestBidsArg); - const hasTsBidder = (mockPbjs.adUnits[0] as any).bids.some( - (b: any) => b.bidder === 'trustedServer' + const hasTsBidder = (mockPbjs.adUnits[0].bids ?? []).some( + (b: TestBid) => b.bidder === 'trustedServer' ); expect(hasTsBidder).toBe(true); }); @@ -774,7 +830,9 @@ describe('prebid/installPrebidNpm', () => { ]); const pbjs = installPrebidNpm(); - pbjs.requestBids({ adUnits: [{ bids: [{ bidder: 'appnexus', params: {} }] }] } as any); + pbjs.requestBids({ + adUnits: [{ bids: [{ bidder: 'appnexus', params: {} }] }], + } as unknown as RequestBidsArg); const cookieValue = document.cookie.match(/(?:^|; )ts-eids=([^;]+)/)?.[1]; expect(cookieValue).toBeDefined(); @@ -797,7 +855,9 @@ describe('prebid/installPrebidNpm', () => { mockGetUserIdsAsEids.mockReturnValue([]); const pbjs = installPrebidNpm(); - pbjs.requestBids({ adUnits: [{ bids: [{ bidder: 'appnexus', params: {} }] }] } as any); + pbjs.requestBids({ + adUnits: [{ bids: [{ bidder: 'appnexus', params: {} }] }], + } as unknown as RequestBidsArg); expect(document.cookie).toBe(''); }); @@ -812,15 +872,15 @@ describe('prebid/installPrebidNpm with server-injected config', () => { mockGetUserIdsAsEids.mockReset(); mockGetUserIdsAsEids.mockReturnValue([]); document.cookie = 'ts-eids=; Path=/; Max-Age=0'; - delete (window as any).__tsjs_prebid; + delete testWindow.__tsjs_prebid; }); afterEach(() => { - delete (window as any).__tsjs_prebid; + delete testWindow.__tsjs_prebid; }); it('reads timeout and debug from window.__tsjs_prebid', () => { - (window as any).__tsjs_prebid = { timeout: 1500, debug: true }; + testWindow.__tsjs_prebid = { timeout: 1500, debug: true }; installPrebidNpm(); @@ -830,7 +890,7 @@ describe('prebid/installPrebidNpm with server-injected config', () => { }); it('explicit config overrides server-injected values', () => { - (window as any).__tsjs_prebid = { timeout: 1500, debug: true }; + testWindow.__tsjs_prebid = { timeout: 1500, debug: true }; installPrebidNpm({ timeout: 3000, debug: false }); @@ -853,13 +913,13 @@ describe('prebid/installRefreshHandler', () => { mockRequestBids.mockReset(); mockPbjs.requestBids = mockRequestBids; mockPbjs.adUnits = []; - (window as any).tsjs = undefined; - delete (window as any).googletag; + testWindow.tsjs = undefined; + delete testWindow.googletag; }); afterEach(() => { - (window as any).tsjs = undefined; - delete (window as any).googletag; + testWindow.tsjs = undefined; + delete testWindow.googletag; }); it('builds refresh ad units from injected slot metadata', () => { @@ -872,11 +932,11 @@ describe('prebid/installRefreshHandler', () => { refresh: originalRefresh, getSlots: vi.fn(() => [gptSlot]), }; - (window as any).googletag = { + testWindow.googletag = { cmd: { push: (fn: () => void) => fn() }, pubads: () => pubads, }; - (window as any).tsjs = { + testWindow.tsjs = { adSlots: [ { id: 'homepage_header_ad', @@ -930,11 +990,11 @@ describe('prebid/installRefreshHandler', () => { refresh: originalRefresh, getSlots: vi.fn(() => [gptSlot]), }; - (window as any).googletag = { + testWindow.googletag = { cmd: { push: (fn: () => void) => fn() }, pubads: () => pubads, }; - (window as any).tsjs = { + testWindow.tsjs = { adSlots: [ { id: 'prefix_ad', @@ -975,7 +1035,7 @@ describe('prebid/installRefreshHandler', () => { it('scopes the GPT targeting call to the refreshed slot code', () => { const setTargetingForGPTAsync = vi.fn(); - (mockPbjs as any).setTargetingForGPTAsync = setTargetingForGPTAsync; + mockPbjs.setTargetingForGPTAsync = setTargetingForGPTAsync; // Run the bidsBackHandler synchronously so the targeting call fires. mockRequestBids.mockImplementation((opts?: { bidsBackHandler?: () => void }) => { opts?.bidsBackHandler?.(); @@ -991,11 +1051,11 @@ describe('prebid/installRefreshHandler', () => { refresh: originalRefresh, getSlots: vi.fn(() => [headerSlot]), }; - (window as any).googletag = { + testWindow.googletag = { cmd: { push: (fn: () => void) => fn() }, pubads: () => pubads, }; - (window as any).tsjs = { + testWindow.tsjs = { adSlots: [ { id: 'header_ad', @@ -1021,11 +1081,11 @@ describe('prebid/installRefreshHandler', () => { expect(setTargetingForGPTAsync).toHaveBeenCalledWith(['div-ad-header']); expect(originalRefresh).toHaveBeenCalledWith([headerSlot], undefined); - delete (mockPbjs as any).setTargetingForGPTAsync; + mockPbjs.setTargetingForGPTAsync = undefined; }); it('includes configured client-side bidders in refresh ad units', () => { - (window as any).__tsjs_prebid = { clientSideBidders: ['rubicon'] }; + testWindow.__tsjs_prebid = { clientSideBidders: ['rubicon'] }; // Original publisher ad unit carries a client-side rubicon bid. mockPbjs.adUnits = [ { @@ -1045,11 +1105,11 @@ describe('prebid/installRefreshHandler', () => { refresh: originalRefresh, getSlots: vi.fn(() => [gptSlot]), }; - (window as any).googletag = { + testWindow.googletag = { cmd: { push: (fn: () => void) => fn() }, pubads: () => pubads, }; - (window as any).tsjs = { + testWindow.tsjs = { adSlots: [ { id: 'homepage_header_ad', @@ -1078,7 +1138,7 @@ describe('prebid/installRefreshHandler', () => { }) ); - delete (window as any).__tsjs_prebid; + delete testWindow.__tsjs_prebid; mockPbjs.adUnits = []; }); @@ -1100,11 +1160,11 @@ describe('prebid/installRefreshHandler', () => { refresh: originalRefresh, getSlots: vi.fn(() => [gptSlot]), }; - (window as any).googletag = { + testWindow.googletag = { cmd: { push: (fn: () => void) => fn() }, pubads: () => pubads, }; - (window as any).tsjs = { + testWindow.tsjs = { adSlots: [ { id: 'homepage_header_ad', @@ -1146,7 +1206,7 @@ describe('prebid/installRefreshHandler', () => { // publisher's Prebid ad unit is keyed by the inner div_id. The synthetic // refresh code stays the GPT element id (so GPT can match it), while params // and client-side bids are recovered from the injected div_id candidate. - (window as any).__tsjs_prebid = { clientSideBidders: ['rubicon'] }; + testWindow.__tsjs_prebid = { clientSideBidders: ['rubicon'] }; mockPbjs.adUnits = [ { code: 'div-ad-x', @@ -1165,11 +1225,11 @@ describe('prebid/installRefreshHandler', () => { refresh: originalRefresh, getSlots: vi.fn(() => [gptSlot]), }; - (window as any).googletag = { + testWindow.googletag = { cmd: { push: (fn: () => void) => fn() }, pubads: () => pubads, }; - (window as any).tsjs = { + testWindow.tsjs = { adSlots: [ { id: 'x_ad', @@ -1205,7 +1265,7 @@ describe('prebid/installRefreshHandler', () => { }) ); - delete (window as any).__tsjs_prebid; + delete testWindow.__tsjs_prebid; mockPbjs.adUnits = []; }); @@ -1233,11 +1293,11 @@ describe('prebid/installRefreshHandler', () => { refresh: originalRefresh, getSlots: vi.fn(() => [gptSlot]), }; - (window as any).googletag = { + testWindow.googletag = { cmd: { push: (fn: () => void) => fn() }, pubads: () => pubads, }; - (window as any).tsjs = { + testWindow.tsjs = { adSlots: [ { id: 'homepage_header_ad', @@ -1295,12 +1355,12 @@ describe('prebid/installRefreshHandler', () => { getSlots: vi.fn(() => [gptSlot]), }; const setTargetingForGPTAsync = vi.fn(); - (mockPbjs as any).setTargetingForGPTAsync = setTargetingForGPTAsync; - (window as any).googletag = { + mockPbjs.setTargetingForGPTAsync = setTargetingForGPTAsync; + testWindow.googletag = { cmd: { push: (fn: () => void) => fn() }, pubads: () => pubads, }; - (window as any).tsjs = { + testWindow.tsjs = { adSlots: [ { id: 'homepage_header_ad', @@ -1365,11 +1425,11 @@ describe('prebid/installRefreshHandler', () => { refresh: originalRefresh, getSlots: vi.fn(() => [gptSlot]), }; - (window as any).googletag = { + testWindow.googletag = { cmd: { push: (fn: () => void) => fn() }, pubads: () => pubads, }; - (window as any).tsjs = { adInitRefreshInProgress: true }; + testWindow.tsjs = { adInitRefreshInProgress: true }; installRefreshHandler(750); pubads.refresh([gptSlot]); @@ -1390,11 +1450,11 @@ describe('prebid/installRefreshHandler', () => { refresh: originalRefresh, getSlots: vi.fn(() => [gptSlot]), }; - (window as any).googletag = { + testWindow.googletag = { cmd: { push: (fn: () => void) => fn() }, pubads: () => pubads, }; - (window as any).tsjs = { adInitRefreshInProgress: false }; + testWindow.tsjs = { adInitRefreshInProgress: false }; installRefreshHandler(750); pubads.refresh([gptSlot]); @@ -1412,16 +1472,16 @@ describe('prebid/client-side bidders', () => { mockGetUserIdsAsEids.mockReset(); mockGetUserIdsAsEids.mockReturnValue([]); // By default the manifest declares all adapters compiled in. - (window as any).__tsjs_prebid_bundle = DEFAULT_BUNDLE_MANIFEST; - delete (window as any).__tsjs_prebid; + testWindow.__tsjs_prebid_bundle = DEFAULT_BUNDLE_MANIFEST; + delete testWindow.__tsjs_prebid; }); afterEach(() => { - delete (window as any).__tsjs_prebid; + delete testWindow.__tsjs_prebid; }); it('excludes client-side bidders from trustedServer bidderParams', () => { - (window as any).__tsjs_prebid = { clientSideBidders: ['rubicon'] }; + testWindow.__tsjs_prebid = { clientSideBidders: ['rubicon'] }; const pbjs = installPrebidNpm(); @@ -1434,9 +1494,9 @@ describe('prebid/client-side bidders', () => { ], }, ]; - pbjs.requestBids({ adUnits } as any); + pbjs.requestBids({ adUnits } as unknown as RequestBidsArg); - const tsBid = adUnits[0].bids.find((b: any) => b.bidder === 'trustedServer') as any; + const tsBid = adUnits[0].bids.find((b: TestBid) => b.bidder === 'trustedServer') as TestBid; expect(tsBid).toBeDefined(); // rubicon should NOT be in bidderParams — it runs client-side expect(tsBid.params.bidderParams).toEqual({ @@ -1446,7 +1506,7 @@ describe('prebid/client-side bidders', () => { }); it('preserves client-side bidder bids as standalone entries', () => { - (window as any).__tsjs_prebid = { clientSideBidders: ['rubicon'] }; + testWindow.__tsjs_prebid = { clientSideBidders: ['rubicon'] }; const pbjs = installPrebidNpm(); @@ -1458,17 +1518,17 @@ describe('prebid/client-side bidders', () => { ], }, ]; - pbjs.requestBids({ adUnits } as any); + pbjs.requestBids({ adUnits } as unknown as RequestBidsArg); // rubicon bid should remain untouched as a standalone entry - const rubiconBid = adUnits[0].bids.find((b: any) => b.bidder === 'rubicon') as any; + const rubiconBid = adUnits[0].bids.find((b: TestBid) => b.bidder === 'rubicon') as TestBid; expect(rubiconBid).toBeDefined(); expect(rubiconBid.params).toEqual({ accountId: 'abc' }); - expect(adUnits[0].bids.find((b: any) => b.bidder === 'appnexus')).toBeUndefined(); + expect(adUnits[0].bids.find((b: TestBid) => b.bidder === 'appnexus')).toBeUndefined(); }); it('handles multiple client-side bidders', () => { - (window as any).__tsjs_prebid = { clientSideBidders: ['rubicon', 'openx'] }; + testWindow.__tsjs_prebid = { clientSideBidders: ['rubicon', 'openx'] }; const pbjs = installPrebidNpm(); @@ -1481,18 +1541,18 @@ describe('prebid/client-side bidders', () => { ], }, ]; - pbjs.requestBids({ adUnits } as any); + pbjs.requestBids({ adUnits } as unknown as RequestBidsArg); - const tsBid = adUnits[0].bids.find((b: any) => b.bidder === 'trustedServer') as any; + const tsBid = adUnits[0].bids.find((b: TestBid) => b.bidder === 'trustedServer') as TestBid; // Only appnexus should be in bidderParams expect(tsBid.params.bidderParams).toEqual({ appnexus: { placementId: 123 }, }); // Both client-side bidders should remain - expect(adUnits[0].bids.find((b: any) => b.bidder === 'rubicon')).toBeDefined(); - expect(adUnits[0].bids.find((b: any) => b.bidder === 'openx')).toBeDefined(); - expect(adUnits[0].bids.find((b: any) => b.bidder === 'appnexus')).toBeUndefined(); + expect(adUnits[0].bids.find((b: TestBid) => b.bidder === 'rubicon')).toBeDefined(); + expect(adUnits[0].bids.find((b: TestBid) => b.bidder === 'openx')).toBeDefined(); + expect(adUnits[0].bids.find((b: TestBid) => b.bidder === 'appnexus')).toBeUndefined(); }); it('behaves normally when no client-side bidders are configured', () => { @@ -1507,9 +1567,9 @@ describe('prebid/client-side bidders', () => { ], }, ]; - pbjs.requestBids({ adUnits } as any); + pbjs.requestBids({ adUnits } as unknown as RequestBidsArg); - const tsBid = adUnits[0].bids.find((b: any) => b.bidder === 'trustedServer') as any; + const tsBid = adUnits[0].bids.find((b: TestBid) => b.bidder === 'trustedServer') as TestBid; expect(tsBid.params.bidderParams).toEqual({ appnexus: { placementId: 123 }, rubicon: { accountId: 'abc' }, @@ -1517,7 +1577,7 @@ describe('prebid/client-side bidders', () => { }); it('behaves normally when client-side bidders list is empty', () => { - (window as any).__tsjs_prebid = { clientSideBidders: [] }; + testWindow.__tsjs_prebid = { clientSideBidders: [] }; const pbjs = installPrebidNpm(); @@ -1529,9 +1589,9 @@ describe('prebid/client-side bidders', () => { ], }, ]; - pbjs.requestBids({ adUnits } as any); + pbjs.requestBids({ adUnits } as unknown as RequestBidsArg); - const tsBid = adUnits[0].bids.find((b: any) => b.bidder === 'trustedServer') as any; + const tsBid = adUnits[0].bids.find((b: TestBid) => b.bidder === 'trustedServer') as TestBid; expect(tsBid.params.bidderParams).toEqual({ appnexus: { placementId: 123 }, rubicon: { accountId: 'abc' }, @@ -1539,7 +1599,7 @@ describe('prebid/client-side bidders', () => { }); it('still injects trustedServer when all bidders are client-side', () => { - (window as any).__tsjs_prebid = { clientSideBidders: ['rubicon', 'appnexus'] }; + testWindow.__tsjs_prebid = { clientSideBidders: ['rubicon', 'appnexus'] }; const pbjs = installPrebidNpm(); @@ -1551,21 +1611,21 @@ describe('prebid/client-side bidders', () => { ], }, ]; - pbjs.requestBids({ adUnits } as any); + pbjs.requestBids({ adUnits } as unknown as RequestBidsArg); // trustedServer should still be present (even with empty bidderParams) - const tsBid = adUnits[0].bids.find((b: any) => b.bidder === 'trustedServer') as any; + const tsBid = adUnits[0].bids.find((b: TestBid) => b.bidder === 'trustedServer') as TestBid; expect(tsBid).toBeDefined(); expect(tsBid.params.bidderParams).toEqual({}); }); it('logs error when a client-side bidder has no adapter in the external bundle', () => { // rubicon is compiled into the external bundle, but openx is not - (window as any).__tsjs_prebid_bundle = { + testWindow.__tsjs_prebid_bundle = { ...DEFAULT_BUNDLE_MANIFEST, adapters: ['rubicon'], }; - (window as any).__tsjs_prebid = { clientSideBidders: ['rubicon', 'openx'] }; + testWindow.__tsjs_prebid = { clientSideBidders: ['rubicon', 'openx'] }; const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); @@ -1591,12 +1651,12 @@ describe('prebid/client-side bidders', () => { expect(hasRubiconError).toBe(false); errorSpy.mockRestore(); - (window as any).__tsjs_prebid_bundle = DEFAULT_BUNDLE_MANIFEST; + testWindow.__tsjs_prebid_bundle = DEFAULT_BUNDLE_MANIFEST; }); it('warns when the external bundle stamped no adapter manifest', () => { - delete (window as any).__tsjs_prebid_bundle; - (window as any).__tsjs_prebid = { clientSideBidders: ['rubicon'] }; + delete testWindow.__tsjs_prebid_bundle; + testWindow.__tsjs_prebid = { clientSideBidders: ['rubicon'] }; const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); @@ -1608,11 +1668,11 @@ describe('prebid/client-side bidders', () => { expect(hasManifestWarn).toBe(true); warnSpy.mockRestore(); - (window as any).__tsjs_prebid_bundle = DEFAULT_BUNDLE_MANIFEST; + testWindow.__tsjs_prebid_bundle = DEFAULT_BUNDLE_MANIFEST; }); it('does not log errors when all client-side bidders have adapters', () => { - (window as any).__tsjs_prebid = { clientSideBidders: ['rubicon'] }; + testWindow.__tsjs_prebid = { clientSideBidders: ['rubicon'] }; const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); diff --git a/crates/trusted-server-js/lib/test/shared/beacon_guard.test.ts b/crates/trusted-server-js/lib/test/shared/beacon_guard.test.ts index 9ade23382..881a4515f 100644 --- a/crates/trusted-server-js/lib/test/shared/beacon_guard.test.ts +++ b/crates/trusted-server-js/lib/test/shared/beacon_guard.test.ts @@ -1,4 +1,5 @@ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; + import { createBeaconGuard, BeaconGuardConfig } from '../../src/shared/beacon_guard'; describe('Beacon Guard', () => { @@ -14,12 +15,10 @@ describe('Beacon Guard', () => { originalFetch = window.fetch; // Create spies that simulate real sendBeacon/fetch behaviour - sendBeaconSpy = vi.fn((_url: string | URL, _data?: BodyInit | null) => true); + sendBeaconSpy = vi.fn(() => true); navigator.sendBeacon = sendBeaconSpy; - fetchSpy = vi.fn((_input: RequestInfo | URL, _init?: RequestInit) => - Promise.resolve(new Response('', { status: 200 })) - ); + fetchSpy = vi.fn(() => Promise.resolve(new Response('', { status: 200 }))); window.fetch = fetchSpy; config = {