diff --git a/integrationExamples/gpt/top-level-paapi/tl_paapi_example.html b/integrationExamples/gpt/top-level-paapi/tl_paapi_example.html
deleted file mode 100644
index 9a4991d27113..000000000000
--- a/integrationExamples/gpt/top-level-paapi/tl_paapi_example.html
+++ /dev/null
@@ -1,188 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-Standalone PAAPI Prebid.js Example
-Start local server with:
-gulp serve-fast --https
-Chrome flags:
---enable-features=CookieDeprecationFacilitatedTesting:label/treatment_1.2/force_eligible/true
- --privacy-sandbox-enrollment-overrides=https://localhost:9999
-Join interest group at https://privacysandbox.openx.net/fledge/advertiser
-
-Div-1
-
-
-
-
-
-
diff --git a/integrationExamples/top-level-paapi/gam-contextual.html b/integrationExamples/top-level-paapi/gam-contextual.html
new file mode 100644
index 000000000000..b51b512e0cac
--- /dev/null
+++ b/integrationExamples/top-level-paapi/gam-contextual.html
@@ -0,0 +1,134 @@
+
+
+
+
+
+
+
+
+
+
+
+GAM contextual + Publisher as top level PAAPI seller example
+
+
+ This example starts PAAPI auctions at the same time as GAM targeting. The flow is
+ similar to a typical GAM auction, but if Prebid wins, and got a
+ PAAPI bid, it is rendered instead of the contextual bid.
+
+
+
+
+Div-1
+
+
+
+
+
Instructions
+
Start local server with:
+
gulp serve-fast --https
+
Chrome flags:
+
--enable-features=CookieDeprecationFacilitatedTesting:label/treatment_1.2/force_eligible/true
+ --privacy-sandbox-enrollment-overrides=https://localhost:9999
+
Join interest group at https://www.optable.co/
+
+
+
+
diff --git a/integrationExamples/top-level-paapi/no_adserver.html b/integrationExamples/top-level-paapi/no_adserver.html
new file mode 100644
index 000000000000..0b37f80f27c8
--- /dev/null
+++ b/integrationExamples/top-level-paapi/no_adserver.html
@@ -0,0 +1,113 @@
+
+
+
+
+
+
+
+
+
+No ad server, publisher as top level PAAPI seller example
+
+
+
+
+
+
+
+Div-1
+
+
+
+
+
Instructions
+
Start local server with:
+
gulp serve-fast --https
+
Chrome flags:
+
--enable-features=CookieDeprecationFacilitatedTesting:label/treatment_1.2/force_eligible/true
+ --privacy-sandbox-enrollment-overrides=https://localhost:9999
+
Join interest group at https://www.optable.co/
+
+
+
+
diff --git a/integrationExamples/gpt/top-level-paapi/decisionLogic.js b/integrationExamples/top-level-paapi/shared/decisionLogic.js
similarity index 100%
rename from integrationExamples/gpt/top-level-paapi/decisionLogic.js
rename to integrationExamples/top-level-paapi/shared/decisionLogic.js
diff --git a/integrationExamples/top-level-paapi/shared/example-setup.js b/integrationExamples/top-level-paapi/shared/example-setup.js
new file mode 100644
index 000000000000..1c52abf02c96
--- /dev/null
+++ b/integrationExamples/top-level-paapi/shared/example-setup.js
@@ -0,0 +1,95 @@
+// intercept navigator.runAdAuction and print parameters to console
+(() => {
+ var originalRunAdAuction = navigator.runAdAuction;
+ navigator.runAdAuction = function (...args) {
+ console.log('%c runAdAuction', 'background: cyan; border: 2px; border-radius: 3px', ...args);
+ return originalRunAdAuction.apply(navigator, args);
+ };
+})();
+init();
+setupContextualResponse();
+
+function addExampleControls(requestBids) {
+ const ctl = document.createElement('div');
+ ctl.innerHTML = `
+
+ Simulate contextual bid:
+
+ CPM
+ BID
+
+ `;
+ ctl.style = 'margin-top: 30px';
+ document.body.appendChild(ctl);
+ ctl.querySelector('.bid').addEventListener('click', function (ev) {
+ const cpm = ctl.querySelector('.cpm').value;
+ if (cpm) {
+ setupContextualResponse(parseInt(cpm, 10));
+ }
+ requestBids();
+ });
+}
+
+function init() {
+ window.pbjs = window.pbjs || {que: []};
+ window.pbjs.que.push(() => {
+ pbjs.aliasBidder('optable', 'contextual');
+ [
+ 'auctionInit',
+ 'auctionTimeout',
+ 'auctionEnd',
+ 'bidAdjustment',
+ 'bidTimeout',
+ 'bidRequested',
+ 'bidResponse',
+ 'bidRejected',
+ 'noBid',
+ 'seatNonBid',
+ 'bidWon',
+ 'bidderDone',
+ 'bidderError',
+ 'setTargeting',
+ 'beforeRequestBids',
+ 'beforeBidderHttp',
+ 'requestBids',
+ 'addAdUnits',
+ 'adRenderFailed',
+ 'adRenderSucceeded',
+ 'tcf2Enforcement',
+ 'auctionDebug',
+ 'bidViewable',
+ 'staleRender',
+ 'billableEvent',
+ 'bidAccepted',
+ 'paapiRunAuction',
+ 'paapiBid',
+ 'paapiNoBid',
+ 'paapiError',
+ ].forEach(evt => {
+ pbjs.onEvent(evt, (arg) => {
+ console.log('Event:', evt, arg);
+ })
+ });
+ });
+}
+
+function setupContextualResponse(cpm = 1) {
+ pbjs.que.push(() => {
+ pbjs.setConfig({
+ debugging: {
+ enabled: true,
+ intercept: [
+ {
+ when: {
+ bidder: 'contextual'
+ },
+ then: {
+ cpm,
+ currency: 'USD'
+ }
+ }
+ ]
+ }
+ });
+ });
+}
diff --git a/libraries/ortbConverter/lib/sizes.js b/libraries/ortbConverter/lib/sizes.js
deleted file mode 100644
index 16b750482036..000000000000
--- a/libraries/ortbConverter/lib/sizes.js
+++ /dev/null
@@ -1,14 +0,0 @@
-import {parseSizesInput} from '../../../src/utils.js';
-
-export function sizesToFormat(sizes) {
- sizes = parseSizesInput(sizes);
-
- // get sizes in form [{ w: , h: }, ...]
- return sizes.map(size => {
- const [width, height] = size.split('x');
- return {
- w: parseInt(width, 10),
- h: parseInt(height, 10)
- };
- });
-}
diff --git a/libraries/ortbConverter/processors/banner.js b/libraries/ortbConverter/processors/banner.js
index 2d0136c84b2b..fca9598022b0 100644
--- a/libraries/ortbConverter/processors/banner.js
+++ b/libraries/ortbConverter/processors/banner.js
@@ -1,6 +1,13 @@
-import {createTrackPixelHtml, deepAccess, encodeMacroURI, inIframe, mergeDeep} from '../../../src/utils.js';
+import {
+ createTrackPixelHtml,
+ deepAccess,
+ inIframe,
+ mergeDeep,
+ sizesToSizeTuples,
+ sizeTupleToRtbSize,
+ encodeMacroURI
+} from '../../../src/utils.js';
import {BANNER} from '../../../src/mediaTypes.js';
-import {sizesToFormat} from '../lib/sizes.js';
/**
* fill in a request `imp` with banner parameters from `bidRequest`.
@@ -14,7 +21,7 @@ export function fillBannerImp(imp, bidRequest, context) {
topframe: inIframe() === true ? 0 : 1
};
if (bannerParams.sizes) {
- banner.format = sizesToFormat(bannerParams.sizes);
+ banner.format = sizesToSizeTuples(bannerParams.sizes).map(sizeTupleToRtbSize);
}
if (bannerParams.hasOwnProperty('pos')) {
banner.pos = bannerParams.pos;
diff --git a/libraries/ortbConverter/processors/video.js b/libraries/ortbConverter/processors/video.js
index c38231d90024..b10ad4032c5a 100644
--- a/libraries/ortbConverter/processors/video.js
+++ b/libraries/ortbConverter/processors/video.js
@@ -1,6 +1,5 @@
-import {deepAccess, isEmpty, logWarn, mergeDeep} from '../../../src/utils.js';
+import {deepAccess, isEmpty, logWarn, mergeDeep, sizesToSizeTuples, sizeTupleToRtbSize} from '../../../src/utils.js';
import {VIDEO} from '../../../src/mediaTypes.js';
-import {sizesToFormat} from '../lib/sizes.js';
// parameters that share the same name & semantics between pbjs adUnits and imp.video
const ORTB_VIDEO_PARAMS = new Set([
@@ -41,7 +40,7 @@ export function fillVideoImp(imp, bidRequest, context) {
.filter(([name]) => ORTB_VIDEO_PARAMS.has(name))
);
if (videoParams.playerSize) {
- const format = sizesToFormat(videoParams.playerSize);
+ const format = sizesToSizeTuples(videoParams.playerSize).map(sizeTupleToRtbSize);
if (format.length > 1) {
logWarn('video request specifies more than one playerSize; all but the first will be ignored')
}
diff --git a/libraries/weakStore/weakStore.js b/libraries/weakStore/weakStore.js
new file mode 100644
index 000000000000..09606354daef
--- /dev/null
+++ b/libraries/weakStore/weakStore.js
@@ -0,0 +1,15 @@
+import {auctionManager} from '../../src/auctionManager.js';
+
+export function weakStore(get) {
+ const store = new WeakMap();
+ return function (id, init = {}) {
+ const obj = get(id);
+ if (obj == null) return;
+ if (!store.has(obj)) {
+ store.set(obj, init);
+ }
+ return store.get(obj);
+ };
+}
+
+export const auctionStore = () => weakStore((auctionId) => auctionManager.index.getAuction({auctionId}));
diff --git a/modules/fledgeForGpt.js b/modules/fledgeForGpt.js
index bda4494faaf9..a356785dbe1a 100644
--- a/modules/fledgeForGpt.js
+++ b/modules/fledgeForGpt.js
@@ -1,8 +1,8 @@
/**
* GPT-specific slot configuration logic for PAAPI.
*/
-import {submodule} from '../src/hook.js';
-import {deepAccess, logInfo, logWarn} from '../src/utils.js';
+import {getHook, submodule} from '../src/hook.js';
+import {deepAccess, logInfo, logWarn, sizeTupleToSizeString} from '../src/utils.js';
import {getGptSlotForAdUnitCode} from '../libraries/gptUtils/gptUtils.js';
import {config} from '../src/config.js';
import {getGlobal} from '../src/prebidGlobal.js';
@@ -12,6 +12,7 @@ import {getGlobal} from '../src/prebidGlobal.js';
// TODO: remove this in prebid 9
// eslint-disable-next-line prebid/validate-imports
import './paapi.js';
+import {keyCompare} from '../src/utils/reducers.js';
const MODULE = 'fledgeForGpt';
let getPAAPIConfig;
@@ -73,6 +74,68 @@ export function onAuctionConfigFactory(setGptConfig = setComponentAuction) {
}
}
+export const getPAAPISizeHook = (() => {
+ /*
+ https://github.com/google/ads-privacy/tree/master/proposals/fledge-multiple-seller-testing#faq
+ https://support.google.com/admanager/answer/1100453?hl=en
+
+ Ignore any placeholder sizes, where placeholder is defined as a square creative with a side of <= 5 pixels
+ Look if there are any sizes that are part of the set of supported ad sizes defined here. If there are, choose the largest supported size by area (width * height)
+ For clarity, the set of supported ad sizes includes all of the ad sizes listed under “Top-performing ad sizes”, “Other supported ad sizes”, and “Regional ad sizes”.
+ If not, choose the largest remaining size (i.e. that isn’t in the list of supported ad sizes) by area (width * height)
+ */
+ const SUPPORTED_SIZES = [
+ [728, 90],
+ [336, 280],
+ [300, 250],
+ [300, 50],
+ [160, 600],
+ [1024, 768],
+ [970, 250],
+ [970, 90],
+ [768, 1024],
+ [480, 320],
+ [468, 60],
+ [320, 480],
+ [320, 100],
+ [320, 50],
+ [300, 600],
+ [300, 100],
+ [250, 250],
+ [234, 60],
+ [200, 200],
+ [180, 150],
+ [125, 125],
+ [120, 600],
+ [120, 240],
+ [120, 60],
+ [88, 31],
+ [980, 120],
+ [980, 90],
+ [950, 90],
+ [930, 180],
+ [750, 300],
+ [750, 200],
+ [750, 100],
+ [580, 400],
+ [250, 360],
+ [240, 400],
+ ].sort(keyCompare(([w, h]) => -(w * h)))
+ .map(size => [size, sizeTupleToSizeString(size)]);
+
+ return function(next, sizes) {
+ if (sizes?.length) {
+ const sizeStrings = new Set(sizes.map(sizeTupleToSizeString));
+ const preferredSize = SUPPORTED_SIZES.find(([_, sizeStr]) => sizeStrings.has(sizeStr));
+ if (preferredSize) {
+ next.bail(preferredSize[0]);
+ return;
+ }
+ }
+ next(sizes);
+ }
+})();
+
export function setPAAPIConfigFactory(
getConfig = (filters) => getPAAPIConfig(filters, true),
setGptConfig = setComponentAuction) {
@@ -105,5 +168,6 @@ submodule('paapi', {
onAuctionConfig: onAuctionConfigFactory(),
init(params) {
getPAAPIConfig = params.getPAAPIConfig;
+ getHook('getPAAPISize').before(getPAAPISizeHook);
}
});
diff --git a/modules/paapi.js b/modules/paapi.js
index 28252d0bb7aa..3d562e83c56c 100644
--- a/modules/paapi.js
+++ b/modules/paapi.js
@@ -2,15 +2,15 @@
* Collect PAAPI component auction configs from bid adapters and make them available through `pbjs.getPAAPIConfig()`
*/
import {config} from '../src/config.js';
-import {getHook, module} from '../src/hook.js';
-import {deepSetValue, logInfo, logWarn, mergeDeep, deepEqual, parseSizesInput, deepAccess} from '../src/utils.js';
+import {getHook, hook, module} from '../src/hook.js';
+import {deepSetValue, logInfo, logWarn, mergeDeep, sizesToSizeTuples, deepAccess, deepEqual} from '../src/utils.js';
import {IMP, PBS, registerOrtbProcessor, RESPONSE} from '../src/pbjsORTB.js';
import * as events from '../src/events.js';
import {EVENTS} from '../src/constants.js';
import {currencyCompare} from '../libraries/currencyUtils/currency.js';
-import {maximum, minimum} from '../src/utils/reducers.js';
-import {auctionManager} from '../src/auctionManager.js';
+import {keyCompare, maximum, minimum} from '../src/utils/reducers.js';
import {getGlobal} from '../src/prebidGlobal.js';
+import {auctionStore} from '../libraries/weakStore/weakStore.js';
const MODULE = 'PAAPI';
@@ -19,23 +19,14 @@ const USED = new WeakSet();
export function registerSubmodule(submod) {
submodules.push(submod);
- submod.init && submod.init({getPAAPIConfig});
+ submod.init && submod.init({
+ getPAAPIConfig,
+ expandFilters
+ });
}
module('paapi', registerSubmodule);
-function auctionStore() {
- const store = new WeakMap();
- return function (auctionId, init = {}) {
- const auction = auctionManager.index.getAuction({auctionId});
- if (auction == null) return;
- if (!store.has(auction)) {
- store.set(auction, init);
- }
- return store.get(auction);
- };
-}
-
const pendingConfigsForAuction = auctionStore();
const configsForAuction = auctionStore();
const pendingBuyersForAuction = auctionStore();
@@ -71,7 +62,7 @@ getHook('addPaapiConfig').before(addPaapiConfigHook);
getHook('makeBidRequests').after(markForFledge);
events.on(EVENTS.AUCTION_END, onAuctionEnd);
-function getSlotSignals(bidsReceived = [], bidRequests = []) {
+function getSlotSignals(adUnit = {}, bidsReceived = [], bidRequests = []) {
let bidfloor, bidfloorcur;
if (bidsReceived.length > 0) {
const bestBid = bidsReceived.reduce(maximum(currencyCompare(bid => [bid.cpm, bid.currency])));
@@ -88,6 +79,10 @@ function getSlotSignals(bidsReceived = [], bidRequests = []) {
deepSetValue(cfg, 'auctionSignals.prebid.bidfloor', bidfloor);
bidfloorcur && deepSetValue(cfg, 'auctionSignals.prebid.bidfloorcur', bidfloorcur);
}
+ const requestedSize = getRequestedSize(adUnit);
+ if (requestedSize) {
+ cfg.requestedSize = requestedSize;
+ }
return cfg;
}
@@ -109,7 +104,7 @@ export function buyersToAuctionConfigs(igbRequests, merge = mergeBuyers, config
}
function onAuctionEnd({auctionId, bidsReceived, bidderRequests, adUnitCodes, adUnits}) {
- const adUnitsByCode = Object.fromEntries(adUnits?.map(au => [au.code, au]) || [])
+ const adUnitsByCode = Object.fromEntries(adUnits?.map(au => [au.code, au]) || []);
const allReqs = bidderRequests?.flatMap(br => br.bids);
const paapiConfigs = {};
(adUnitCodes || []).forEach(au => {
@@ -125,23 +120,11 @@ function onAuctionEnd({auctionId, bidsReceived, bidderRequests, adUnitCodes, adU
}
Object.entries(pendingConfigs || {}).forEach(([adUnitCode, auctionConfigs]) => {
const forThisAdUnit = (bid) => bid.adUnitCode === adUnitCode;
- const slotSignals = getSlotSignals(bidsReceived?.filter(forThisAdUnit), allReqs?.filter(forThisAdUnit));
+ const slotSignals = getSlotSignals(adUnitsByCode[adUnitCode], bidsReceived?.filter(forThisAdUnit), allReqs?.filter(forThisAdUnit));
paapiConfigs[adUnitCode] = {
...slotSignals,
componentAuctions: auctionConfigs.map(cfg => mergeDeep({}, slotSignals, cfg))
};
- // TODO: need to flesh out size treatment:
- // - which size should the paapi auction pick? (this uses the first one defined)
- // - should we signal it to SSPs, and how?
- // - what should we do if adapters pick a different one?
- // - what does size mean for video and native?
- const size = parseSizesInput(adUnitsByCode[adUnitCode]?.mediaTypes?.banner?.sizes)?.[0]?.split('x');
- if (size) {
- paapiConfigs[adUnitCode].requestedSize = {
- width: size[0],
- height: size[1],
- };
- }
latestAuctionForAdUnit[adUnitCode] = auctionId;
});
configsForAuction(auctionId, paapiConfigs);
@@ -263,33 +246,54 @@ export function partitionBuyersByBidder(igbRequests) {
})
return Object.entries(igbs).map(([bidder, igbs]) => [requests[bidder], igbs])
}
+
+/**
+ * Expand PAAPI api filters into a map from ad unit code to auctionId.
+ *
+ * @param auctionId when specified, the result will have this as the value for each entry.
+ * when not specified, each ad unit will map to the latest auction that involved that ad unit.
+ * @param adUnitCode when specified, the result will contain only one entry (for this ad unit) or be empty (if this ad
+ * unit was never involved in an auction).
+ * when not specified, the result will contain an entry for every ad unit that was involved in any auction.
+ * @return {{[adUnitCode: string]: string}}
+ */
+function expandFilters({auctionId, adUnitCode} = {}) {
+ let adUnitCodes = [];
+ if (adUnitCode == null) {
+ adUnitCodes = Object.keys(latestAuctionForAdUnit);
+ } else if (latestAuctionForAdUnit.hasOwnProperty(adUnitCode)) {
+ adUnitCodes = [adUnitCode];
+ }
+ return Object.fromEntries(
+ adUnitCodes.map(au => [au, auctionId ?? latestAuctionForAdUnit[au]])
+ );
+}
+
/**
* Get PAAPI auction configuration.
*
- * @param auctionId? optional auction filter; if omitted, the latest auction for each ad unit is used
- * @param adUnitCode? optional ad unit filter
+ * @param filters
+ * @param filters.auctionId? optional auction filter; if omitted, the latest auction for each ad unit is used
+ * @param filters.adUnitCode? optional ad unit filter
* @param includeBlanks if true, include null entries for ad units that match the given filters but do not have any available auction configs.
* @returns {{}} a map from ad unit code to auction config for the ad unit.
*/
-export function getPAAPIConfig({auctionId, adUnitCode} = {}, includeBlanks = false) {
+export function getPAAPIConfig(filters = {}, includeBlanks = false) {
const output = {};
- const targetedAuctionConfigs = auctionId && configsForAuction(auctionId);
- Object.keys((auctionId != null ? targetedAuctionConfigs : latestAuctionForAdUnit) ?? []).forEach(au => {
- const latestAuctionId = latestAuctionForAdUnit[au];
- const auctionConfigs = targetedAuctionConfigs ?? (latestAuctionId && configsForAuction(latestAuctionId));
- if ((adUnitCode ?? au) === au) {
- let candidate;
- if (targetedAuctionConfigs?.hasOwnProperty(au)) {
- candidate = targetedAuctionConfigs[au];
- } else if (auctionId == null && auctionConfigs?.hasOwnProperty(au)) {
- candidate = auctionConfigs[au];
- }
+ Object.entries(expandFilters(filters)).forEach(([au, auctionId]) => {
+ const auctionConfigs = configsForAuction(auctionId);
+ if (auctionConfigs?.hasOwnProperty(au)) {
+ // ad unit was involved in a PAAPI auction
+ const candidate = auctionConfigs[au];
if (candidate && !USED.has(candidate)) {
output[au] = candidate;
USED.add(candidate);
} else if (includeBlanks) {
output[au] = null;
}
+ } else if (auctionId == null && includeBlanks) {
+ // ad unit was involved in a non-PAAPI auction
+ output[au] = null;
}
});
return output;
@@ -310,6 +314,29 @@ function getFledgeConfig() {
};
}
+/**
+ * Given an array of size tuples, return the one that should be used for PAAPI.
+ */
+export const getPAAPISize = hook('sync', function (sizes) {
+ if (sizes?.length) {
+ return sizes
+ .filter(([w, h]) => !(w === h && w <= 5))
+ .reduce(maximum(keyCompare(([w, h]) => w * h)));
+ }
+}, 'getPAAPISize');
+
+function getRequestedSize(adUnit) {
+ return adUnit.ortb2Imp?.ext?.paapi?.requestedSize || (() => {
+ const size = getPAAPISize(sizesToSizeTuples(adUnit.mediaTypes?.banner?.sizes));
+ if (size) {
+ return {
+ width: size[0],
+ height: size[1]
+ };
+ }
+ })();
+}
+
export function markForFledge(next, bidderRequests) {
if (isFledgeSupported()) {
bidderRequests.forEach((bidderReq) => {
@@ -338,6 +365,10 @@ export function markForFledge(next, bidderRequests) {
ae: bidAe,
biddable: 1
}, bidReq.ortb2Imp.ext.igs)
+ const requestedSize = getRequestedSize(bidReq);
+ if (requestedSize) {
+ deepSetValue(bidReq, 'ortb2Imp.ext.paapi.requestedSize', requestedSize);
+ }
}
});
});
diff --git a/modules/topLevelPaapi.js b/modules/topLevelPaapi.js
new file mode 100644
index 000000000000..040c0125b3a4
--- /dev/null
+++ b/modules/topLevelPaapi.js
@@ -0,0 +1,215 @@
+import {submodule} from '../src/hook.js';
+import {config} from '../src/config.js';
+import {logError, logInfo, logWarn, mergeDeep} from '../src/utils.js';
+import {auctionStore} from '../libraries/weakStore/weakStore.js';
+import {getGlobal} from '../src/prebidGlobal.js';
+import {emit} from '../src/events.js';
+import {BID_STATUS, EVENTS} from '../src/constants.js';
+import {GreedyPromise} from '../src/utils/promise.js';
+import {getBidToRender, getRenderingData, markWinningBid} from '../src/adRendering.js';
+
+let getPAAPIConfig, expandFilters, moduleConfig;
+
+const paapiBids = auctionStore();
+const MODULE_NAME = 'topLevelPaapi';
+
+config.getConfig('paapi', (cfg) => {
+ moduleConfig = cfg.paapi?.topLevelSeller;
+ if (moduleConfig) {
+ getBidToRender.before(renderPaapiHook);
+ getBidToRender.after(renderOverrideHook);
+ getRenderingData.before(getRenderingDataHook);
+ markWinningBid.before(markWinningBidHook);
+ } else {
+ getBidToRender.getHooks({hook: renderPaapiHook}).remove();
+ getBidToRender.getHooks({hook: renderOverrideHook}).remove();
+ getRenderingData.getHooks({hook: getRenderingDataHook}).remove();
+ markWinningBid.getHooks({hook: markWinningBidHook}).remove();
+ }
+});
+
+function isPaapiBid(bid) {
+ return bid?.source === 'paapi';
+}
+
+function bidIfRenderable(bid) {
+ if (bid && !bid.urn) {
+ logWarn(MODULE_NAME, 'rendering in fenced frames is not supported. Consider using resolveToConfig: false', bid);
+ return;
+ }
+ return bid;
+}
+
+const forRenderStack = [];
+
+function renderPaapiHook(next, adId, forRender = true, override = GreedyPromise.resolve()) {
+ forRenderStack.push(forRender);
+ const ids = parsePaapiAdId(adId);
+ if (ids) {
+ override = override.then((bid) => {
+ if (bid) return bid;
+ const [auctionId, adUnitCode] = ids;
+ return paapiBids(auctionId)?.[adUnitCode]?.then(bid => {
+ if (!bid) {
+ logWarn(MODULE_NAME, `No PAAPI bid found for auctionId: "${auctionId}", adUnit: "${adUnitCode}"`);
+ }
+ return bidIfRenderable(bid);
+ });
+ });
+ }
+ next(adId, forRender, override);
+}
+
+function renderOverrideHook(next, bidPm) {
+ const forRender = forRenderStack.pop();
+ if (moduleConfig?.overrideWinner) {
+ bidPm = bidPm.then((bid) => {
+ if (isPaapiBid(bid) || bid?.status === BID_STATUS.RENDERED) return bid;
+ return getPAAPIBids({adUnitCode: bid.adUnitCode}).then(res => {
+ let paapiBid = bidIfRenderable(res[bid.adUnitCode]);
+ if (paapiBid) {
+ if (!forRender) return paapiBid;
+ if (forRender && paapiBid.status !== BID_STATUS.RENDERED) {
+ paapiBid.overriddenAdId = bid.adId;
+ logInfo(MODULE_NAME, 'overriding contextual bid with PAAPI bid', bid, paapiBid)
+ return paapiBid;
+ }
+ }
+ return bid;
+ });
+ });
+ }
+ next(bidPm);
+}
+
+export function getRenderingDataHook(next, bid, options) {
+ if (isPaapiBid(bid)) {
+ next.bail({
+ width: bid.width,
+ height: bid.height,
+ adUrl: bid.urn
+ });
+ } else {
+ next(bid, options);
+ }
+}
+
+export function markWinningBidHook(next, bid) {
+ if (isPaapiBid(bid)) {
+ bid.status = BID_STATUS.RENDERED;
+ emit(EVENTS.BID_WON, bid);
+ next.bail();
+ } else {
+ next(bid);
+ }
+}
+
+function getBaseAuctionConfig() {
+ if (moduleConfig?.auctionConfig) {
+ return Object.assign({
+ resolveToConfig: false
+ }, moduleConfig.auctionConfig);
+ }
+}
+
+function onAuctionConfig(auctionId, auctionConfigs) {
+ const base = getBaseAuctionConfig();
+ if (base) {
+ Object.entries(auctionConfigs).forEach(([adUnitCode, auctionConfig]) => {
+ mergeDeep(auctionConfig, base);
+ if (moduleConfig.autorun ?? true) {
+ getPAAPIBids({adUnitCode, auctionId});
+ }
+ });
+ }
+}
+
+export function parsePaapiSize(size) {
+ /* From https://github.com/WICG/turtledove/blob/main/FLEDGE.md#12-interest-group-attributes:
+ * Each size has the format {width: widthVal, height: heightVal},
+ * where the values can have either pixel units (e.g. 100 or '100px') or screen dimension coordinates (e.g. 100sw or 100sh).
+ */
+ if (typeof size === 'number') return size;
+ if (typeof size === 'string') {
+ const px = /^(\d+)(px)?$/.exec(size)?.[1];
+ if (px) {
+ return parseInt(px, 10);
+ }
+ }
+ return null;
+}
+
+export function getPaapiAdId(auctionId, adUnitCode) {
+ return `paapi:/${auctionId.replace(/:/g, '::')}/:/${adUnitCode.replace(/:/g, '::')}`;
+}
+
+export function parsePaapiAdId(adId) {
+ const match = /^paapi:\/(.*)\/:\/(.*)$/.exec(adId);
+ if (match) {
+ return [match[1], match[2]].map(s => s.replace(/::/g, ':'));
+ }
+}
+
+/**
+ * Returns the PAAPI runAdAuction result for the given filters, as a map from ad unit code to auction result
+ * (an object with `width`, `height`, and one of `urn` or `frameConfig`).
+ *
+ * @param filters
+ * @param raa
+ * @return {Promise<{[p: string]: any}>}
+ */
+export function getPAAPIBids(filters, raa = (...args) => navigator.runAdAuction(...args)) {
+ return Promise.all(
+ Object.entries(expandFilters(filters))
+ .map(([adUnitCode, auctionId]) => {
+ const bids = paapiBids(auctionId);
+ if (bids && !bids.hasOwnProperty(adUnitCode)) {
+ const auctionConfig = getPAAPIConfig({adUnitCode, auctionId})[adUnitCode];
+ if (auctionConfig) {
+ emit(EVENTS.RUN_PAAPI_AUCTION, {
+ auctionId,
+ adUnitCode,
+ auctionConfig
+ });
+ bids[adUnitCode] = new Promise((resolve, reject) => raa(auctionConfig).then(resolve, reject))
+ .then(result => {
+ if (result) {
+ const bid = {
+ source: 'paapi',
+ adId: getPaapiAdId(auctionId, adUnitCode),
+ width: parsePaapiSize(auctionConfig.requestedSize?.width),
+ height: parsePaapiSize(auctionConfig.requestedSize?.height),
+ adUnitCode,
+ auctionId,
+ [typeof result === 'string' ? 'urn' : 'frameConfig']: result,
+ auctionConfig,
+ };
+ emit(EVENTS.PAAPI_BID, bid);
+ return bid;
+ } else {
+ emit(EVENTS.PAAPI_NO_BID, {auctionId, adUnitCode, auctionConfig});
+ return null;
+ }
+ }).catch(error => {
+ logError(MODULE_NAME, `error (auction "${auctionId}", adUnit "${adUnitCode}"):`, error);
+ emit(EVENTS.PAAPI_ERROR, {auctionId, adUnitCode, error, auctionConfig});
+ return null;
+ });
+ }
+ }
+ return bids?.[adUnitCode]?.then(res => [adUnitCode, res]);
+ }).filter(e => e)
+ ).then(result => Object.fromEntries(result));
+}
+
+getGlobal().getPAAPIBids = (filters) => getPAAPIBids(filters);
+
+export const topLevelPAAPI = {
+ name: MODULE_NAME,
+ init(params) {
+ getPAAPIConfig = params.getPAAPIConfig;
+ expandFilters = params.expandFilters;
+ },
+ onAuctionConfig
+};
+submodule('paapi', topLevelPAAPI);
diff --git a/src/adRendering.js b/src/adRendering.js
index 7d306adc9cc3..33f7fe9252c7 100644
--- a/src/adRendering.js
+++ b/src/adRendering.js
@@ -8,10 +8,22 @@ import {auctionManager} from './auctionManager.js';
import {getCreativeRenderer} from './creativeRenderers.js';
import {hook} from './hook.js';
import {fireNativeTrackers} from './native.js';
+import {GreedyPromise} from './utils/promise.js';
const { AD_RENDER_FAILED, AD_RENDER_SUCCEEDED, STALE_RENDER, BID_WON } = EVENTS;
const { EXCEPTION } = AD_RENDER_FAILED_REASON;
+export const getBidToRender = hook('sync', function (adId, forRender = true, override = GreedyPromise.resolve()) {
+ return override
+ .then(bid => bid ?? auctionManager.findBidByAdId(adId))
+ .catch(() => {})
+})
+
+export const markWinningBid = hook('sync', function (bid) {
+ events.emit(BID_WON, bid);
+ auctionManager.addWinningBid(bid);
+})
+
/**
* Emit the AD_RENDER_FAILED event.
*
@@ -168,8 +180,7 @@ export function handleRender({renderFn, resizeFn, adId, options, bidResponse, do
bid: bidResponse
});
}
- auctionManager.addWinningBid(bidResponse);
- events.emit(BID_WON, bidResponse);
+ markWinningBid(bidResponse);
}
export function renderAdDirect(doc, adId, options) {
@@ -211,12 +222,13 @@ export function renderAdDirect(doc, adId, options) {
if (!adId || !doc) {
fail(AD_RENDER_FAILED_REASON.MISSING_DOC_OR_ADID, `missing ${adId ? 'doc' : 'adId'}`);
} else {
- bid = auctionManager.findBidByAdId(adId);
-
if ((doc === document && !inIframe())) {
fail(AD_RENDER_FAILED_REASON.PREVENT_WRITING_ON_MAIN_DOCUMENT, `renderAd was prevented from writing to the main document.`);
} else {
- handleRender({renderFn, resizeFn, adId, options: {clickUrl: options?.clickThrough}, bidResponse: bid, doc});
+ getBidToRender(adId).then(bidResponse => {
+ bid = bidResponse;
+ handleRender({renderFn, resizeFn, adId, options: {clickUrl: options?.clickThrough}, bidResponse, doc});
+ });
}
}
} catch (e) {
diff --git a/src/constants.js b/src/constants.js
index bb76083862b5..4ca5f6a1b125 100644
--- a/src/constants.js
+++ b/src/constants.js
@@ -41,7 +41,11 @@ export const EVENTS = {
BID_VIEWABLE: 'bidViewable',
STALE_RENDER: 'staleRender',
BILLABLE_EVENT: 'billableEvent',
- BID_ACCEPTED: 'bidAccepted'
+ BID_ACCEPTED: 'bidAccepted',
+ RUN_PAAPI_AUCTION: 'paapiRunAuction',
+ PAAPI_BID: 'paapiBid',
+ PAAPI_NO_BID: 'paapiNoBid',
+ PAAPI_ERROR: 'paapiError',
};
export const AD_RENDER_FAILED_REASON = {
diff --git a/src/secureCreatives.js b/src/secureCreatives.js
index 96ace0792e48..a33f742b7383 100644
--- a/src/secureCreatives.js
+++ b/src/secureCreatives.js
@@ -3,19 +3,15 @@
access to a publisher page from creative payloads.
*/
-import * as events from './events.js';
import {getAllAssetsMessage, getAssetMessage} from './native.js';
-import { BID_STATUS, EVENTS, MESSAGES } from './constants.js';
+import {BID_STATUS, MESSAGES} from './constants.js';
import {isApnGetTagDefined, isGptPubadsDefined, logError, logWarn} from './utils.js';
-import {auctionManager} from './auctionManager.js';
import {find, includes} from './polyfill.js';
-import {handleCreativeEvent, handleNativeMessage, handleRender} from './adRendering.js';
+import {getBidToRender, handleCreativeEvent, handleNativeMessage, handleRender, markWinningBid} from './adRendering.js';
import {getCreativeRendererSource} from './creativeRenderers.js';
const { REQUEST, RESPONSE, NATIVE, EVENT } = MESSAGES;
-const BID_WON = EVENTS.BID_WON;
-
const HANDLER_MAP = {
[REQUEST]: handleRenderRequest,
[EVENT]: handleEventRequest,
@@ -28,7 +24,9 @@ if (FEATURES.NATIVE) {
}
export function listenMessagesFromCreative() {
- window.addEventListener('message', receiveMessage, false);
+ window.addEventListener('message', function (ev) {
+ receiveMessage(ev);
+ }, false);
}
export function getReplier(ev) {
@@ -49,6 +47,12 @@ export function getReplier(ev) {
}
}
+function ensureAdId(adId, reply) {
+ return function (data, ...args) {
+ return reply(Object.assign({}, data, {adId}), ...args);
+ }
+}
+
export function receiveMessage(ev) {
var key = ev.message ? 'message' : 'data';
var data = {};
@@ -58,19 +62,19 @@ export function receiveMessage(ev) {
return;
}
- if (data && data.adId && data.message) {
- const adObject = find(auctionManager.getBidsReceived(), function (bid) {
- return bid.adId === data.adId;
- });
- if (HANDLER_MAP.hasOwnProperty(data.message)) {
- HANDLER_MAP[data.message](getReplier(ev), data, adObject);
- }
+ if (data && data.adId && data.message && HANDLER_MAP.hasOwnProperty(data.message)) {
+ return getBidToRender(data.adId, data.message === MESSAGES.REQUEST).then(adObject => {
+ HANDLER_MAP[data.message](ensureAdId(data.adId, getReplier(ev)), data, adObject);
+ })
}
}
-function getResizer(bidResponse) {
+function getResizer(adId, bidResponse) {
+ // in some situations adId !== bidResponse.adId
+ // the first is the one that was requested and is tied to the element
+ // the second is the one that is being rendered (sometimes different, e.g. in some paapi setups)
return function (width, height) {
- resizeRemoteCreative({...bidResponse, width, height});
+ resizeRemoteCreative({...bidResponse, width, height, adId});
}
}
function handleRenderRequest(reply, message, bidResponse) {
@@ -81,7 +85,7 @@ function handleRenderRequest(reply, message, bidResponse) {
renderer: getCreativeRendererSource(bidResponse)
}, adData));
},
- resizeFn: getResizer(bidResponse),
+ resizeFn: getResizer(message.adId, bidResponse),
options: message.options,
adId: message.adId,
bidResponse
@@ -100,8 +104,7 @@ function handleNativeRequest(reply, data, adObject) {
}
if (adObject.status !== BID_STATUS.RENDERED) {
- auctionManager.addWinningBid(adObject);
- events.emit(BID_WON, adObject);
+ markWinningBid(adObject);
}
switch (data.action) {
diff --git a/src/utils.js b/src/utils.js
index 46dd06a6a417..12656951c212 100644
--- a/src/utils.js
+++ b/src/utils.js
@@ -130,37 +130,55 @@ export function transformAdServerTargetingObj(targeting) {
}
/**
- * Parse a GPT-Style general size Array like `[[300, 250]]` or `"300x250,970x90"` into an array of sizes `["300x250"]` or '['300x250', '970x90']'
- * @param {(Array.|Array.)} sizeObj Input array or double array [300,250] or [[300,250], [728,90]]
- * @return {Array.} Array of strings like `["300x250"]` or `["300x250", "728x90"]`
+ * Parse a GPT-Style general size Array like `[[300, 250]]` or `"300x250,970x90"` into an array of width, height tuples `[[300, 250]]` or '[[300,250], [970,90]]'
*/
-export function parseSizesInput(sizeObj) {
- if (typeof sizeObj === 'string') {
+export function sizesToSizeTuples(sizes) {
+ if (typeof sizes === 'string') {
// multiple sizes will be comma-separated
- return sizeObj.split(',').filter(sz => sz.match(/^(\d)+x(\d)+$/i))
- } else if (typeof sizeObj === 'object') {
- if (sizeObj.length === 2 && typeof sizeObj[0] === 'number' && typeof sizeObj[1] === 'number') {
- return [parseGPTSingleSizeArray(sizeObj)];
- } else {
- return sizeObj.map(parseGPTSingleSizeArray)
+ return sizes
+ .split(/\s*,\s*/)
+ .map(sz => sz.match(/^(\d+)x(\d+)$/i))
+ .filter(match => match)
+ .map(([_, w, h]) => [parseInt(w, 10), parseInt(h, 10)])
+ } else if (Array.isArray(sizes)) {
+ if (isValidGPTSingleSize(sizes)) {
+ return [sizes]
}
+ return sizes.filter(isValidGPTSingleSize);
}
return [];
}
+/**
+ * Parse a GPT-Style general size Array like `[[300, 250]]` or `"300x250,970x90"` into an array of sizes `["300x250"]` or '['300x250', '970x90']'
+ * @param {(Array.|Array.)} sizeObj Input array or double array [300,250] or [[300,250], [728,90]]
+ * @return {Array.} Array of strings like `["300x250"]` or `["300x250", "728x90"]`
+ */
+export function parseSizesInput(sizeObj) {
+ return sizesToSizeTuples(sizeObj).map(sizeTupleToSizeString);
+}
+
+export function sizeTupleToSizeString(size) {
+ return size[0] + 'x' + size[1]
+}
+
// Parse a GPT style single size array, (i.e [300, 250])
// into an AppNexus style string, (i.e. 300x250)
export function parseGPTSingleSizeArray(singleSize) {
if (isValidGPTSingleSize(singleSize)) {
- return singleSize[0] + 'x' + singleSize[1];
+ return sizeTupleToSizeString(singleSize);
}
}
+export function sizeTupleToRtbSize(size) {
+ return {w: size[0], h: size[1]};
+}
+
// Parse a GPT style single size array, (i.e [300, 250])
// into OpenRTB-compatible (imp.banner.w/h, imp.banner.format.w/h, imp.video.w/h) object(i.e. {w:300, h:250})
export function parseGPTSingleSizeArrayToRtbSize(singleSize) {
if (isValidGPTSingleSize(singleSize)) {
- return {w: singleSize[0], h: singleSize[1]};
+ return sizeTupleToRtbSize(singleSize)
}
}
diff --git a/test/spec/libraries/weakStore_spec.js b/test/spec/libraries/weakStore_spec.js
new file mode 100644
index 000000000000..407b83391ef0
--- /dev/null
+++ b/test/spec/libraries/weakStore_spec.js
@@ -0,0 +1,32 @@
+import {weakStore} from '../../../libraries/weakStore/weakStore.js';
+
+describe('weakStore', () => {
+ let targets, store;
+ beforeEach(() => {
+ targets = {
+ id: {}
+ };
+ store = weakStore((id) => targets[id]);
+ });
+
+ it('returns undef if getter returns undef', () => {
+ expect(store('missing')).to.not.exist;
+ });
+
+ it('inits to empty object by default', () => {
+ expect(store('id')).to.eql({});
+ });
+
+ it('inits to given value', () => {
+ expect(store('id', {initial: 'value'})).to.eql({'initial': 'value'});
+ });
+
+ it('returns the same object as long as the target does not change', () => {
+ expect(store('id')).to.equal(store('id'));
+ });
+
+ it('ignores init value if already initialized', () => {
+ store('id', {initial: 'value'});
+ expect(store('id', {second: 'value'})).to.eql({initial: 'value'});
+ })
+});
diff --git a/test/spec/modules/fledgeForGpt_spec.js b/test/spec/modules/fledgeForGpt_spec.js
index 8ab111711213..aa513f931db0 100644
--- a/test/spec/modules/fledgeForGpt_spec.js
+++ b/test/spec/modules/fledgeForGpt_spec.js
@@ -1,4 +1,9 @@
-import {onAuctionConfigFactory, setPAAPIConfigFactory, slotConfigurator} from 'modules/fledgeForGpt.js';
+import {
+ getPAAPISizeHook,
+ onAuctionConfigFactory,
+ setPAAPIConfigFactory,
+ slotConfigurator
+} from 'modules/fledgeForGpt.js';
import * as gptUtils from '../../../libraries/gptUtils/gptUtils.js';
import 'modules/appnexusBidAdapter.js';
import 'modules/rubiconBidAdapter.js';
@@ -173,5 +178,29 @@ describe('fledgeForGpt module', () => {
sinon.assert.calledWith(setGptConfig, au, config?.componentAuctions ?? [], true);
})
});
+ });
+
+ describe('getPAAPISizeHook', () => {
+ let next;
+ beforeEach(() => {
+ next = sinon.stub();
+ next.bail = sinon.stub();
+ });
+
+ it('should pick largest supported size over larger unsupported size', () => {
+ getPAAPISizeHook(next, [[999, 999], [300, 250], [300, 600], [1234, 4321]]);
+ sinon.assert.calledWith(next.bail, [300, 600]);
+ });
+
+ Object.entries({
+ 'present': [],
+ 'supported': [[123, 4], [321, 5]],
+ 'defined': undefined,
+ }).forEach(([t, sizes]) => {
+ it(`should defer to next when no size is ${t}`, () => {
+ getPAAPISizeHook(next, sizes);
+ sinon.assert.calledWith(next, sizes);
+ })
+ })
})
});
diff --git a/test/spec/modules/paapi_spec.js b/test/spec/modules/paapi_spec.js
index bc1faa23833c..768e2ba88535 100644
--- a/test/spec/modules/paapi_spec.js
+++ b/test/spec/modules/paapi_spec.js
@@ -2,31 +2,32 @@ import {expect} from 'chai';
import {config} from '../../../src/config.js';
import adapterManager from '../../../src/adapterManager.js';
import * as utils from '../../../src/utils.js';
+import {deepAccess, deepClone} from '../../../src/utils.js';
import {hook} from '../../../src/hook.js';
import 'modules/appnexusBidAdapter.js';
import 'modules/rubiconBidAdapter.js';
import {
addPaapiConfigHook,
+ buyersToAuctionConfigs,
getPAAPIConfig,
- parseExtPrebidFledge,
- registerSubmodule,
- setImpExtAe,
- setResponsePaapiConfigs,
- reset,
- parseExtIgi,
- mergeBuyers,
+ getPAAPISize,
IGB_TO_CONFIG,
+ mergeBuyers,
+ parseExtIgi,
+ parseExtPrebidFledge,
partitionBuyers,
partitionBuyersByBidder,
- buyersToAuctionConfigs
+ registerSubmodule,
+ reset,
+ setImpExtAe,
+ setResponsePaapiConfigs
} from 'modules/paapi.js';
import * as events from 'src/events.js';
-import { EVENTS } from 'src/constants.js';
+import {EVENTS} from 'src/constants.js';
import {getGlobal} from '../../../src/prebidGlobal.js';
import {auctionManager} from '../../../src/auctionManager.js';
import {stubAuctionIndex} from '../../helpers/indexStub.js';
import {AuctionIndex} from '../../../src/auctionIndex.js';
-import {deepAccess, deepClone} from '../../../src/utils.js';
describe('paapi module', () => {
let sandbox;
@@ -44,6 +45,24 @@ describe('paapi module', () => {
'paapi'
].forEach(configNS => {
describe(`using ${configNS} for configuration`, () => {
+ let getPAAPISizeStub;
+
+ function getPAAPISizeHook(next, sizes) {
+ next.bail(getPAAPISizeStub(sizes));
+ }
+
+ before(() => {
+ getPAAPISize.before(getPAAPISizeHook, 100);
+ });
+
+ after(() => {
+ getPAAPISize.getHooks({hook: getPAAPISizeHook}).remove();
+ });
+
+ beforeEach(() => {
+ getPAAPISizeStub = sinon.stub();
+ });
+
describe('getPAAPIConfig', function () {
let nextFnSpy, auctionConfig, paapiConfig;
before(() => {
@@ -56,7 +75,7 @@ describe('paapi module', () => {
};
paapiConfig = {
config: auctionConfig
- }
+ };
nextFnSpy = sinon.spy();
});
@@ -80,11 +99,11 @@ describe('paapi module', () => {
};
igb2 = {
origin: 'buyer2'
- }
+ };
buyerAuctionConfig = {
seller: 'seller',
decisionLogicURL: 'seller-decision-logic'
- }
+ };
config.mergeConfig({
[configNS]: {
componentSeller: {
@@ -106,14 +125,14 @@ describe('paapi module', () => {
sinon.assert.match(buyerConfig, {
interestGroupBuyers: [igb1.origin, igb2.origin],
...buyerAuctionConfig
- })
+ });
});
describe('FPD', () => {
let ortb2, ortb2Imp;
beforeEach(() => {
ortb2 = {'fpd': 1};
- ortb2Imp = {'fpd': 2}
+ ortb2Imp = {'fpd': 2};
});
function getBuyerAuctionConfig() {
@@ -128,7 +147,7 @@ describe('paapi module', () => {
ortb2,
ortb2Imp
}
- })
+ });
});
it('should not override existing perBuyerSignals', () => {
@@ -139,11 +158,11 @@ describe('paapi module', () => {
};
igb1.pbs = {
prebid: deepClone(original)
- }
+ };
sinon.assert.match(getBuyerAuctionConfig().perBuyerSignals[igb1.origin], {
prebid: original
- })
- })
+ });
+ });
});
});
@@ -187,11 +206,11 @@ describe('paapi module', () => {
const cfg = getPAAPIConfig({}, true);
expect(Object.keys(cfg)).to.have.members(['au1', 'au2', 'au3']);
expect(cfg.au3).to.eql(null);
- })
+ });
it('includes the targeted adUnit', () => {
expect(getPAAPIConfig({adUnitCode: 'au3'}, true)).to.eql({
au3: null
- })
+ });
});
it('includes the targeted auction', () => {
const cfg = getPAAPIConfig({auctionId}, true);
@@ -203,41 +222,17 @@ describe('paapi module', () => {
});
it('does not include non-existing auctions', () => {
expect(getPAAPIConfig({auctionId: 'other'})).to.eql({});
- })
+ });
});
});
it('should drop auction configs after end of auction', () => {
- events.emit(EVENTS.AUCTION_END, { auctionId });
+ events.emit(EVENTS.AUCTION_END, {auctionId});
addPaapiConfigHook(nextFnSpy, {auctionId, adUnitCode: 'au'}, paapiConfig);
- events.emit(EVENTS.AUCTION_END, { auctionId });
+ events.emit(EVENTS.AUCTION_END, {auctionId});
expect(getPAAPIConfig({auctionId})).to.eql({});
});
- it('should use first size as requestedSize', () => {
- addPaapiConfigHook(nextFnSpy, {
- auctionId,
- adUnitCode: 'au1',
- }, paapiConfig);
- events.emit(EVENTS.AUCTION_END, {
- auctionId,
- adUnits: [
- {
- code: 'au1',
- mediaTypes: {
- banner: {
- sizes: [[200, 100], [300, 200]]
- }
- }
- }
- ]
- });
- expect(getPAAPIConfig({auctionId}).au1.requestedSize).to.eql({
- width: '200',
- height: '100'
- })
- })
-
describe('FPD', () => {
let ortb2, ortb2Imp;
beforeEach(() => {
@@ -259,17 +254,17 @@ describe('paapi module', () => {
it('should be added to auctionSignals', () => {
sinon.assert.match(getComponentAuctionConfig().auctionSignals, {
prebid: {ortb2, ortb2Imp}
- })
+ });
});
it('should not override existing auctionSignals', () => {
- auctionConfig.auctionSignals = {prebid: {ortb2: {fpd: 'original'}}}
+ auctionConfig.auctionSignals = {prebid: {ortb2: {fpd: 'original'}}};
sinon.assert.match(getComponentAuctionConfig().auctionSignals, {
prebid: {
ortb2: {fpd: 'original'},
ortb2Imp
}
- })
- })
+ });
+ });
it('should be added to perBuyerSignals', () => {
auctionConfig.interestGroupBuyers = ['buyer1', 'buyer2'];
@@ -277,7 +272,7 @@ describe('paapi module', () => {
sinon.assert.match(pbs, {
buyer1: {prebid: {ortb2, ortb2Imp}},
buyer2: {prebid: {ortb2, ortb2Imp}}
- })
+ });
});
it('should not override existing perBuyerSignals', () => {
@@ -288,13 +283,13 @@ describe('paapi module', () => {
fpd: 'original'
}
}
- }
+ };
auctionConfig.perBuyerSignals = {
buyer: deepClone(original)
- }
+ };
sinon.assert.match(getComponentAuctionConfig().perBuyerSignals.buyer, original);
});
- })
+ });
describe('submodules', () => {
let submods;
@@ -309,7 +304,7 @@ describe('paapi module', () => {
describe('onAuctionConfig', () => {
const auctionId = 'aid';
it('is invoked with null configs when there\'s no config', () => {
- events.emit(EVENTS.AUCTION_END, { auctionId, adUnitCodes: ['au'] });
+ events.emit(EVENTS.AUCTION_END, {auctionId, adUnitCodes: ['au']});
submods.forEach(submod => sinon.assert.calledWith(submod.onAuctionConfig, auctionId, {au: null}));
});
it('is invoked with relevant configs', () => {
@@ -321,7 +316,7 @@ describe('paapi module', () => {
au1: {componentAuctions: [auctionConfig]},
au2: {componentAuctions: [auctionConfig]},
au3: null
- })
+ });
});
});
it('removes configs from getPAAPIConfig if the module calls markAsUsed', () => {
@@ -336,7 +331,7 @@ describe('paapi module', () => {
addPaapiConfigHook(nextFnSpy, {auctionId, adUnitCode: 'au1'}, paapiConfig);
events.emit(EVENTS.AUCTION_END, {auctionId, adUnitCodes: ['au1']});
expect(getPAAPIConfig()).to.not.be.empty;
- })
+ });
});
});
@@ -450,6 +445,65 @@ describe('paapi module', () => {
});
});
});
+
+ describe('requestedSize', () => {
+ let adUnit;
+ beforeEach(() => {
+ adUnit = {
+ code: 'au',
+ };
+ });
+
+ function getConfig() {
+ addPaapiConfigHook(nextFnSpy, {auctionId, adUnitCode: adUnit.code}, paapiConfig);
+ events.emit(EVENTS.AUCTION_END, {auctionId, adUnitCodes: [adUnit.code], adUnits: [adUnit]});
+ return getPAAPIConfig()[adUnit.code];
+ }
+
+ Object.entries({
+ 'adUnit.ortb2Imp.ext.paapi.requestedSize'() {
+ adUnit.ortb2Imp = {
+ ext: {
+ paapi: {
+ requestedSize: {
+ width: 123,
+ height: 321
+ }
+ }
+ }
+ };
+ },
+ 'largest size'() {
+ getPAAPISizeStub.returns([123, 321]);
+ }
+ }).forEach(([t, setup]) => {
+ describe(`should be set from ${t}`, () => {
+ beforeEach(setup);
+
+ it('without overriding component auctions, if set', () => {
+ auctionConfig.requestedSize = {width: '1px', height: '2px'};
+ expect(getConfig().componentAuctions[0].requestedSize).to.eql({
+ width: '1px',
+ height: '2px'
+ });
+ });
+
+ it('on component auction, if missing', () => {
+ expect(getConfig().componentAuctions[0].requestedSize).to.eql({
+ width: 123,
+ height: 321
+ });
+ });
+
+ it('on top level auction', () => {
+ expect(getConfig().requestedSize).to.eql({
+ width: 123,
+ height: 321,
+ });
+ });
+ });
+ });
+ });
});
describe('with multiple auctions', () => {
@@ -485,7 +539,7 @@ describe('paapi module', () => {
configs[auctionId][adUnitCode] = cfg;
addPaapiConfigHook(nextFnSpy, {auctionId, adUnitCode}, {config: cfg});
});
- events.emit(EVENTS.AUCTION_END, { auctionId, adUnitCodes: adUnitCodes.concat(noConfigAdUnitCodes) });
+ events.emit(EVENTS.AUCTION_END, {auctionId, adUnitCodes: adUnitCodes.concat(noConfigAdUnitCodes)});
});
});
@@ -553,15 +607,16 @@ describe('paapi module', () => {
} else {
expect(cfg[au]).to.be.null;
}
- })
- })
- })
+ });
+ });
+ });
});
});
});
describe('markForFledge', function () {
const navProps = Object.fromEntries(['runAdAuction', 'joinAdInterestGroup'].map(p => [p, navigator[p]]));
+ let adUnits;
before(function () {
// navigator.runAdAuction & co may not exist, so we can't stub it normally with
@@ -577,27 +632,30 @@ describe('paapi module', () => {
Object.entries(navProps).forEach(([p, orig]) => navigator[p] = orig);
});
+ beforeEach(() => {
+ getPAAPISizeStub = sinon.stub();
+ adUnits = [{
+ 'code': '/19968336/header-bid-tag1',
+ 'mediaTypes': {
+ 'banner': {
+ 'sizes': [[728, 90]]
+ },
+ },
+ 'bids': [
+ {
+ 'bidder': 'appnexus',
+ },
+ {
+ 'bidder': 'rubicon',
+ },
+ ]
+ }];
+ });
+
afterEach(function () {
config.resetConfig();
});
- const adUnits = [{
- 'code': '/19968336/header-bid-tag1',
- 'mediaTypes': {
- 'banner': {
- 'sizes': [[728, 90]]
- },
- },
- 'bids': [
- {
- 'bidder': 'appnexus',
- },
- {
- 'bidder': 'rubicon',
- },
- ]
- }];
-
function mark() {
return Object.fromEntries(
adapterManager.makeBidRequests(
@@ -628,7 +686,7 @@ describe('paapi module', () => {
biddable: 1
});
}
- })
+ });
}
describe('with setBidderConfig()', () => {
@@ -693,8 +751,8 @@ describe('paapi module', () => {
}
});
Object.values(mark()).forEach(br => expect(br.paapi?.componentSeller).to.eql(componentSeller));
- })
- })
+ });
+ });
it('should not override pub-defined ext.ae', () => {
config.setConfig({
@@ -717,7 +775,7 @@ describe('paapi module', () => {
});
Object.assign(adUnits[0], {ortb2Imp: {ext: {ae: 3}}});
expectFledgeFlags({enabled: true, ae: 3}, {enabled: true, ae: 3});
- })
+ });
it('should not override pub-defined ext.igs', () => {
config.setConfig({
@@ -734,8 +792,8 @@ describe('paapi module', () => {
ae: 1,
biddable: 0
}
- })
- })
+ });
+ });
});
it('should fill ext.ae from ext.igs, if defined', () => {
@@ -745,7 +803,61 @@ describe('paapi module', () => {
}
});
Object.assign(adUnits[0], {ortb2Imp: {ext: {igs: {}}}});
- expectFledgeFlags({enabled: true, ae: 1}, {enabled: true, ae: 1})
+ expectFledgeFlags({enabled: true, ae: 1}, {enabled: true, ae: 1});
+ });
+ });
+
+ describe('ortb2Imp.ext.paapi.requestedSize', () => {
+ beforeEach(() => {
+ config.setConfig({
+ [configNS]: {
+ enabled: true,
+ defaultForSlots: 1,
+ }
+ });
+ });
+
+ it('should default to value returned by getPAAPISize', () => {
+ getPAAPISizeStub.returns([123, 321]);
+ Object.values(mark()).flatMap(b => b.bids).forEach(bidRequest => {
+ sinon.assert.match(bidRequest.ortb2Imp.ext.paapi, {
+ requestedSize: {
+ width: 123,
+ height: 321
+ }
+ });
+ });
+ });
+
+ it('should not be overridden, if provided by the pub', () => {
+ adUnits[0].ortb2Imp = {
+ ext: {
+ paapi: {
+ requestedSize: {
+ width: '123px',
+ height: '321px'
+ }
+ }
+ }
+ };
+ Object.values(mark()).flatMap(b => b.bids).forEach(bidRequest => {
+ sinon.assert.match(bidRequest.ortb2Imp.ext.paapi, {
+ requestedSize: {
+ width: '123px',
+ height: '321px'
+ }
+ });
+ });
+ sinon.assert.notCalled(getPAAPISizeStub);
+ });
+
+ it('should not be set if adUnit has no banner sizes', () => {
+ adUnits[0].mediaTypes = {
+ video: {}
+ };
+ Object.values(mark()).flatMap(b => b.bids).forEach(bidRequest => {
+ expect(bidRequest.ortb2Imp?.ext?.paapi?.requestedSize).to.not.exist;
+ });
});
});
});
@@ -778,7 +890,7 @@ describe('paapi module', () => {
ps: {
priority: 2
}
- }
+ };
});
describe('mergeBuyers', () => {
@@ -831,7 +943,7 @@ describe('paapi module', () => {
it('ignores igbs with duplicate origin', () => {
igb2.origin = igb1.origin;
expect(mergeBuyers([igb1, igb2])).to.eql(mergeBuyers([igb1]));
- })
+ });
});
describe('partitionBuyers', () => {
@@ -841,7 +953,7 @@ describe('paapi module', () => {
it('should ignore igbs that have no origin', () => {
delete igb1.origin;
expect(partitionBuyers([igb1, igb2])).to.eql([[igb2]]);
- })
+ });
it('should return a single partition when duplicates exist, but do not conflict', () => {
expect(partitionBuyers([igb1, igb2, deepClone(igb1)])).to.eql([[igb1, igb2]]);
});
@@ -855,7 +967,7 @@ describe('paapi module', () => {
[igb3],
[igb4]
]);
- })
+ });
});
describe('partitionBuyersByBidder', () => {
@@ -875,8 +987,8 @@ describe('paapi module', () => {
])).to.eql([
[{bidder: 'a', extra: 'data'}, [igb1, igb2]],
[{bidder: 'b', more: 'data'}, [igb1, igb2]]
- ])
- })
+ ]);
+ });
describe('buyersToAuctionConfig', () => {
let config, partitioners, merge, igbRequests;
beforeEach(() => {
@@ -884,11 +996,11 @@ describe('paapi module', () => {
auctionConfig: {
decisionLogicURL: 'mock-decision-logic'
}
- }
+ };
partitioners = {
compact: sinon.stub(),
expand: sinon.stub(),
- }
+ };
let i = 0;
merge = sinon.stub().callsFake(() => ({config: i++}));
igbRequests = [
@@ -911,7 +1023,7 @@ describe('paapi module', () => {
sinon.assert.match(cf2, {
...config.auctionConfig,
config: 1
- })
+ });
sinon.assert.calledWith(partitioners.compact, igbRequests);
[1, 2].forEach(mockPart => sinon.assert.calledWith(merge, mockPart));
});
@@ -933,13 +1045,42 @@ describe('paapi module', () => {
const fpd = {
ortb2: {fpd: 1},
ortb2Imp: {fpd: 2}
- }
+ };
partitioners.compact.returns([[{}], [fpd]]);
const [cf1, cf2] = toAuctionConfig();
expect(cf1.auctionSignals?.prebid).to.not.exist;
expect(cf2.auctionSignals.prebid).to.eql(fpd);
- })
- })
+ });
+ });
+ });
+ });
+
+ describe('getPAAPISize', () => {
+ before(() => {
+ getPAAPISize.getHooks().remove();
+ });
+
+ Object.entries({
+ 'ignores placeholders': {
+ in: [[1, 1], [0, 0], [3, 4]],
+ out: [3, 4]
+ },
+ 'picks largest size by area': {
+ in: [[200, 100], [150, 150]],
+ out: [150, 150]
+ },
+ 'can handle no sizes': {
+ in: [],
+ out: undefined
+ },
+ 'can handle no input': {
+ in: undefined,
+ out: undefined
+ }
+ }).forEach(([t, {in: input, out}]) => {
+ it(t, () => {
+ expect(getPAAPISize(input)).to.eql(out);
+ });
});
});
@@ -958,7 +1099,7 @@ describe('paapi module', () => {
igs: {
biddable: 0
}
- })
+ });
});
describe('response parsing', () => {
@@ -1001,7 +1142,7 @@ describe('paapi module', () => {
igs: configs
}]
}
- }
+ };
},
'ext.igi.igs with impid on igi'(configs) {
return {
@@ -1012,10 +1153,10 @@ describe('paapi module', () => {
return {
impid,
igs: [cfg]
- }
+ };
})
}
- }
+ };
},
'ext.igi.igs with conflicting impid'(configs) {
return {
@@ -1025,7 +1166,7 @@ describe('paapi module', () => {
igs: configs
}]
}
- }
+ };
}
}
}
@@ -1058,9 +1199,9 @@ describe('paapi module', () => {
parser({}, resp, ctx);
expect(extractResult('config', ctx.impContext)).to.eql({});
});
- })
- })
- })
+ });
+ });
+ });
});
describe('response ext.igi.igb', () => {
@@ -1092,15 +1233,15 @@ describe('paapi module', () => {
}
]
}
- }
+ };
parseExtIgi({}, resp, ctx);
expect(extractResult('igb', ctx.impContext)).to.eql({
e1: [1, 2],
e2: [3],
});
- })
- })
- })
+ });
+ });
+ });
describe('setResponsePaapiConfigs', () => {
it('should set paapi configs/igb paired with their corresponding bid id', () => {
diff --git a/test/spec/modules/topLevelPaapi_spec.js b/test/spec/modules/topLevelPaapi_spec.js
new file mode 100644
index 000000000000..e2cad9593e94
--- /dev/null
+++ b/test/spec/modules/topLevelPaapi_spec.js
@@ -0,0 +1,502 @@
+import {
+ addPaapiConfigHook,
+ getPAAPIConfig,
+ registerSubmodule,
+ reset as resetPaapi
+} from '../../../modules/paapi.js';
+import {config} from 'src/config.js';
+import {BID_STATUS, EVENTS} from 'src/constants.js';
+import * as events from 'src/events.js';
+import {
+ getPaapiAdId,
+ getPAAPIBids,
+ getRenderingDataHook, markWinningBidHook,
+ parsePaapiAdId,
+ parsePaapiSize, resizeCreativeHook,
+ topLevelPAAPI
+} from '/modules/topLevelPaapi.js';
+import {auctionManager} from '../../../src/auctionManager.js';
+import {expect} from 'chai/index.js';
+import {getBidToRender} from '../../../src/adRendering.js';
+
+describe('topLevelPaapi', () => {
+ let sandbox, auctionConfig, next, auctionId, auctions;
+ before(() => {
+ resetPaapi();
+ });
+ beforeEach(() => {
+ registerSubmodule(topLevelPAAPI);
+ });
+ afterEach(() => {
+ resetPaapi();
+ });
+ beforeEach(() => {
+ sandbox = sinon.createSandbox();
+ auctions = {};
+ sandbox.stub(auctionManager.index, 'getAuction').callsFake(({auctionId}) => auctions[auctionId]?.auction);
+ next = sinon.stub();
+ auctionId = 'auct';
+ auctionConfig = {
+ seller: 'mock.seller'
+ };
+ config.setConfig({
+ paapi: {
+ enabled: true,
+ defaultForSlots: 1
+ }
+ });
+ });
+ afterEach(() => {
+ config.resetConfig();
+ sandbox.restore();
+ });
+
+ function addPaapiConfig(adUnitCode, auctionConfig, _auctionId = auctionId) {
+ let auction = auctions[_auctionId];
+ if (!auction) {
+ auction = auctions[_auctionId] = {
+ auction: {},
+ adUnits: {}
+ };
+ }
+ if (!auction.adUnits.hasOwnProperty(adUnitCode)) {
+ auction.adUnits[adUnitCode] = {
+ code: adUnitCode,
+ ortb2Imp: {
+ ext: {
+ paapi: {
+ requestedSize: {
+ width: 123,
+ height: 321
+ }
+ }
+ }
+ }
+ };
+ }
+ addPaapiConfigHook(next, {adUnitCode, auctionId: _auctionId}, {
+ config: {
+ ...auctionConfig,
+ auctionId: _auctionId,
+ adUnitCode
+ }
+ });
+ }
+
+ function endAuctions() {
+ Object.entries(auctions).forEach(([auctionId, {adUnits}]) => {
+ events.emit(EVENTS.AUCTION_END, {auctionId, adUnitCodes: Object.keys(adUnits), adUnits: Object.values(adUnits)});
+ });
+ }
+
+ describe('when configured', () => {
+ let auctionConfig;
+ beforeEach(() => {
+ auctionConfig = {
+ seller: 'top.seller',
+ decisionLogicURL: 'https://top.seller/decision-logic.js'
+ };
+ config.mergeConfig({
+ paapi: {
+ topLevelSeller: {
+ auctionConfig,
+ autorun: false
+ }
+ }
+ });
+ });
+
+ it('should augment config returned by getPAAPIConfig', () => {
+ addPaapiConfig('au', auctionConfig);
+ endAuctions();
+ sinon.assert.match(getPAAPIConfig().au, auctionConfig);
+ });
+
+ it('should not choke if auction config is not defined', () => {
+ const cfg = config.getConfig('paapi');
+ delete cfg.topLevelSeller.auctionConfig;
+ config.setConfig(cfg);
+ addPaapiConfig('au', auctionConfig);
+ endAuctions();
+ expect(getPAAPIConfig().au.componentAuctions).to.exist;
+ });
+
+ it('should default resolveToConfig: false', () => {
+ addPaapiConfig('au', auctionConfig);
+ endAuctions();
+ expect(getPAAPIConfig()['au'].resolveToConfig).to.eql(false);
+ });
+
+ describe('when autoRun is set', () => {
+ let origRaa;
+ beforeEach(() => {
+ origRaa = navigator.runAdAuction;
+ navigator.runAdAuction = sinon.stub();
+ });
+ afterEach(() => {
+ navigator.runAdAuction = origRaa;
+ });
+
+ it('should start auctions automatically, when autoRun is set', () => {
+ config.mergeConfig({
+ paapi: {
+ topLevelSeller: {
+ autorun: true
+ }
+ }
+ })
+ addPaapiConfig('au', auctionConfig);
+ endAuctions();
+ sinon.assert.called(navigator.runAdAuction);
+ });
+ });
+
+ describe('getPAAPIBids', () => {
+ Object.entries({
+ 'a string URN': {
+ pack: (val) => val,
+ unpack: (urn) => ({urn}),
+ canRender: true,
+ },
+ 'a frameConfig object': {
+ pack: (val) => ({val}),
+ unpack: (val) => ({frameConfig: {val}}),
+ canRender: false
+ }
+ }).forEach(([t, {pack, unpack, canRender}]) => {
+ describe(`when runAdAuction returns ${t}`, () => {
+ let raa;
+ beforeEach(() => {
+ raa = sinon.stub().callsFake((cfg) => {
+ const {auctionId, adUnitCode} = cfg.componentAuctions[0];
+ return Promise.resolve(pack(`raa-${adUnitCode}-${auctionId}`));
+ });
+ });
+
+ function getBids(filters) {
+ return getPAAPIBids(filters, raa);
+ }
+
+ function expectBids(actual, expected) {
+ expect(Object.keys(actual)).to.eql(Object.keys(expected));
+ Object.entries(expected).forEach(([au, val]) => {
+ sinon.assert.match(actual[au], val == null ? val : {
+ adId: sinon.match(val => parsePaapiAdId(val)[1] === au),
+ width: 123,
+ height: 321,
+ source: 'paapi',
+ ...unpack(val)
+ });
+ });
+ }
+
+ describe('with one auction config', () => {
+ beforeEach(() => {
+ addPaapiConfig('au', auctionConfig, 'auct');
+ endAuctions();
+ });
+ it('should resolve to raa result', () => {
+ return getBids({adUnitCode: 'au', auctionId}).then(result => {
+ sinon.assert.calledWith(raa, sinon.match({
+ ...auctionConfig,
+ componentAuctions: sinon.match(cmp => cmp.find(cfg => sinon.match(cfg, auctionConfig)))
+ }));
+ expectBids(result, {au: 'raa-au-auct'});
+ });
+ });
+
+ Object.entries({
+ 'returns null': () => Promise.resolve(),
+ 'throws': () => { throw new Error() },
+ 'rejects': () => Promise.reject(new Error())
+ }).forEach(([t, behavior]) => {
+ it('should resolve to null when runAdAuction returns null', () => {
+ raa = sinon.stub().callsFake(behavior);
+ return getBids({adUnitCode: 'au', auctionId: 'auct'}).then(result => {
+ expectBids(result, {au: null});
+ });
+ });
+ })
+
+ it('should resolve to the same result when called again', () => {
+ getBids({adUnitCode: 'au', auctionId});
+ return getBids({adUnitCode: 'au', auctionId: 'auct'}).then(result => {
+ sinon.assert.calledOnce(raa);
+ expectBids(result, {au: 'raa-au-auct'});
+ });
+ });
+
+ describe('events', () => {
+ beforeEach(() => {
+ sandbox.stub(events, 'emit');
+ });
+ it('should fire PAAPI_RUN_AUCTION', () => {
+ return Promise.all([
+ getBids({adUnitCode: 'au', auctionId}),
+ getBids({adUnitCode: 'other', auctionId})
+ ]).then(() => {
+ sinon.assert.calledWith(events.emit, EVENTS.RUN_PAAPI_AUCTION, {
+ adUnitCode: 'au',
+ auctionId,
+ auctionConfig: sinon.match(auctionConfig)
+ });
+ sinon.assert.neverCalledWith(events.emit, EVENTS.RUN_PAAPI_AUCTION, {
+ adUnitCode: 'other'
+ });
+ });
+ });
+ it('should fire PAAPI_BID', () => {
+ return getBids({adUnitCode: 'au', auctionId}).then(() => {
+ sinon.assert.calledWith(events.emit, EVENTS.PAAPI_BID, sinon.match({
+ ...unpack('raa-au-auct'),
+ adUnitCode: 'au',
+ auctionId: 'auct'
+ }));
+ });
+ });
+ it('should fire PAAPI_NO_BID', () => {
+ raa = sinon.stub().callsFake(() => Promise.resolve(null));
+ return getBids({adUnitCode: 'au', auctionId}).then(() => {
+ sinon.assert.calledWith(events.emit, EVENTS.PAAPI_NO_BID, sinon.match({
+ adUnitCode: 'au',
+ auctionId: 'auct'
+ }));
+ });
+ });
+
+ it('should fire PAAPI_ERROR', () => {
+ raa = sinon.stub().callsFake(() => Promise.reject(new Error('message')));
+ return getBids({adUnitCode: 'au', auctionId}).then(res => {
+ expect(res).to.eql({au: null});
+ sinon.assert.calledWith(events.emit, EVENTS.PAAPI_ERROR, sinon.match({
+ adUnitCode: 'au',
+ auctionId: 'auct',
+ error: sinon.match({message: 'message'})
+ }));
+ });
+ });
+ });
+
+ it('should hook into getBidToRender', () => {
+ return getBids({adUnitCode: 'au', auctionId}).then(res => {
+ return getBidToRender(res.au.adId).then(bidToRender => [res.au, bidToRender])
+ }).then(([paapiBid, bidToRender]) => {
+ if (canRender) {
+ expect(bidToRender).to.eql(paapiBid)
+ } else {
+ expect(bidToRender).to.not.exist;
+ }
+ });
+ });
+
+ describe('when overrideWinner is set', () => {
+ let mockContextual;
+ beforeEach(() => {
+ mockContextual = {
+ adId: 'mock',
+ adUnitCode: 'au'
+ }
+ sandbox.stub(auctionManager, 'findBidByAdId').returns(mockContextual);
+ config.mergeConfig({
+ paapi: {
+ topLevelSeller: {
+ overrideWinner: true
+ }
+ }
+ });
+ });
+
+ it(`should ${!canRender ? 'NOT' : ''} override winning bid for the same adUnit`, () => {
+ return Promise.all([
+ getBids({adUnitCode: 'au', auctionId}).then(res => res.au),
+ getBidToRender(mockContextual.adId)
+ ]).then(([paapiBid, bidToRender]) => {
+ if (canRender) {
+ expect(bidToRender).to.eql(paapiBid);
+ expect(paapiBid.overriddenAdId).to.eql(mockContextual.adId);
+ } else {
+ expect(bidToRender).to.eql(mockContextual)
+ }
+ })
+ });
+
+ it('should not override when the ad unit has no paapi winner', () => {
+ mockContextual.adUnitCode = 'other';
+ return getBidToRender(mockContextual.adId).then(bidToRender => {
+ expect(bidToRender).to.eql(mockContextual);
+ })
+ });
+
+ it('should not override when already a paapi bid', () => {
+ return getBids({adUnitCode: 'au', auctionId}).then(res => {
+ return getBidToRender(res.au.adId).then((bidToRender) => [bidToRender, res.au]);
+ }).then(([bidToRender, paapiBid]) => {
+ expect(bidToRender).to.eql(canRender ? paapiBid : mockContextual)
+ })
+ });
+
+ if (canRender) {
+ it('should not not override when the bid was already rendered', () => {
+ getBids();
+ return getBidToRender(mockContextual.adId).then((bid) => {
+ // first pass - paapi wins over contextual
+ expect(bid.source).to.eql('paapi');
+ bid.status = BID_STATUS.RENDERED;
+ return getBidToRender(mockContextual.adId, false).then(bidToRender => [bid, bidToRender])
+ }).then(([paapiBid, bidToRender]) => {
+ // if `forRender` = false (bit retrieved for x-domain events and such)
+ // the referenced bid is still paapi
+ expect(bidToRender).to.eql(paapiBid);
+ return getBidToRender(mockContextual.adId);
+ }).then(bidToRender => {
+ // second pass, paapi has been rendered, contextual should win
+ expect(bidToRender).to.eql(mockContextual);
+ bidToRender.status = BID_STATUS.RENDERED;
+ return getBidToRender(mockContextual.adId, false);
+ }).then(bidToRender => {
+ // if the contextual bid has been rendered, it's the one being referenced
+ expect(bidToRender).to.eql(mockContextual);
+ });
+ })
+ }
+ });
+ });
+
+ it('should resolve the same result from different filters', () => {
+ const targets = {
+ auct1: ['au1', 'au2'],
+ auct2: ['au1', 'au3']
+ };
+ Object.entries(targets).forEach(([auctionId, adUnitCodes]) => {
+ adUnitCodes.forEach(au => addPaapiConfig(au, auctionConfig, auctionId));
+ });
+ endAuctions();
+ return Promise.all(
+ [
+ [
+ {adUnitCode: 'au1', auctionId: 'auct1'},
+ {
+ au1: 'raa-au1-auct1'
+ }
+ ],
+ [
+ {},
+ {
+ au1: 'raa-au1-auct2',
+ au2: 'raa-au2-auct1',
+ au3: 'raa-au3-auct2'
+ }
+ ],
+ [
+ {auctionId: 'auct1'},
+ {
+ au1: 'raa-au1-auct1',
+ au2: 'raa-au2-auct1'
+ }
+ ],
+ [
+ {adUnitCode: 'au1'},
+ {
+ au1: 'raa-au1-auct2'
+ }
+ ],
+ ].map(([filters, expected]) => getBids(filters).then(res => [res, expected]))
+ ).then(res => {
+ res.forEach(([actual, expected]) => {
+ expectBids(actual, expected);
+ });
+ });
+ });
+ });
+ });
+ });
+ });
+
+ describe('when not configured', () => {
+ it('should not alter configs returned by getPAAPIConfig', () => {
+ addPaapiConfig('au', auctionConfig);
+ endAuctions();
+ expect(getPAAPIConfig().au.seller).to.not.exist;
+ });
+ });
+
+ describe('paapi adId', () => {
+ [
+ ['auctionId', 'adUnitCode'],
+ ['auction:id', 'adUnit:code'],
+ ['auction:uid', 'ad:unit']
+ ].forEach(([auctionId, adUnitCode]) => {
+ it(`can encode and decode ${auctionId}, ${adUnitCode}`, () => {
+ expect(parsePaapiAdId(getPaapiAdId(auctionId, adUnitCode))).to.eql([auctionId, adUnitCode]);
+ });
+ });
+
+ [undefined, null, 'not-a-paapi-ad', 'paapi:/malformed'].forEach(adId => {
+ it(`returns null for adId ${adId}`, () => {
+ expect(parsePaapiAdId(adId)).to.not.exist;
+ });
+ });
+ });
+
+ describe('parsePaapiSize', () => {
+ [
+ [null, null],
+ [undefined, null],
+ [123, 123],
+ ['123', 123],
+ ['123px', 123],
+ ['1sw', null],
+ ['garbage', null]
+ ].forEach(([input, expected]) => {
+ it(`can parse ${input} => ${expected}`, () => {
+ expect(parsePaapiSize(input)).to.eql(expected);
+ });
+ });
+ });
+
+ describe('rendering hooks', () => {
+ let next;
+ beforeEach(() => {
+ next = sinon.stub()
+ next.bail = sinon.stub()
+ });
+ describe('getRenderingDataHook', () => {
+ it('intercepts paapi bids', () => {
+ getRenderingDataHook(next, {
+ source: 'paapi',
+ width: 123,
+ height: null,
+ urn: 'url'
+ });
+ sinon.assert.calledWith(next.bail, {
+ width: 123,
+ height: null,
+ adUrl: 'url'
+ });
+ });
+ it('does not touch non-paapi bids', () => {
+ getRenderingDataHook(next, {bid: 'data'}, {other: 'options'});
+ sinon.assert.calledWith(next, {bid: 'data'}, {other: 'options'});
+ });
+ });
+
+ describe('markWinnigBidsHook', () => {
+ beforeEach(() => {
+ sandbox.stub(events, 'emit');
+ });
+ it('handles paapi bids', () => {
+ const bid = {source: 'paapi'};
+ markWinningBidHook(next, bid);
+ sinon.assert.notCalled(next);
+ sinon.assert.called(next.bail);
+ expect(bid.status).to.eql(BID_STATUS.RENDERED);
+ sinon.assert.calledWith(events.emit, EVENTS.BID_WON, bid);
+ });
+ it('ignores non-paapi bids', () => {
+ markWinningBidHook(next, {other: 'bid'});
+ sinon.assert.calledWith(next, {other: 'bid'});
+ sinon.assert.notCalled(next.bail);
+ });
+ });
+ });
+});
diff --git a/test/spec/unit/adRendering_spec.js b/test/spec/unit/adRendering_spec.js
index df837e5547e1..4d0962a0b2c2 100644
--- a/test/spec/unit/adRendering_spec.js
+++ b/test/spec/unit/adRendering_spec.js
@@ -1,7 +1,7 @@
import * as events from 'src/events.js';
import * as utils from 'src/utils.js';
import {
- doRender,
+ doRender, getBidToRender,
getRenderingData,
handleCreativeEvent,
handleNativeMessage,
@@ -24,6 +24,28 @@ describe('adRendering', () => {
sandbox.restore();
})
+ describe('getBidToRender', () => {
+ beforeEach(() => {
+ sandbox.stub(auctionManager, 'findBidByAdId').callsFake(() => 'auction-bid')
+ });
+ it('should default to bid from auctionManager', () => {
+ return getBidToRender('adId', true, Promise.resolve(null)).then((res) => {
+ expect(res).to.eql('auction-bid');
+ sinon.assert.calledWith(auctionManager.findBidByAdId, 'adId');
+ });
+ });
+ it('should give precedence to override promise', () => {
+ return getBidToRender('adId', true, Promise.resolve('override')).then((res) => {
+ expect(res).to.eql('override');
+ sinon.assert.notCalled(auctionManager.findBidByAdId);
+ })
+ });
+ it('should return undef when override rejects', () => {
+ return getBidToRender('adId', true, Promise.reject(new Error('any reason'))).then(res => {
+ expect(res).to.not.exist;
+ })
+ })
+ })
describe('getRenderingData', () => {
let bidResponse;
beforeEach(() => {
diff --git a/test/spec/unit/pbjs_api_spec.js b/test/spec/unit/pbjs_api_spec.js
index deb80873cfa6..94643f34a050 100644
--- a/test/spec/unit/pbjs_api_spec.js
+++ b/test/spec/unit/pbjs_api_spec.js
@@ -28,6 +28,7 @@ import {mockFpdEnrichments} from '../../helpers/fpd.js';
import {generateUUID} from '../../../src/utils.js';
import {getCreativeRenderer} from '../../../src/creativeRenderers.js';
import { BID_STATUS, EVENTS, GRANULARITY_OPTIONS, TARGETING_KEYS } from 'src/constants.js';
+import {getBidToRender} from '../../../src/adRendering.js';
var assert = require('chai').assert;
var expect = require('chai').expect;
@@ -201,12 +202,16 @@ window.apntag = {
describe('Unit: Prebid Module', function () {
let bidExpiryStub, sandbox;
-
+ function getBidToRenderHook(next, adId) {
+ // make sure we can handle async bidToRender
+ next(adId, new Promise((resolve) => setTimeout(resolve)))
+ }
before((done) => {
hook.ready();
$$PREBID_GLOBAL$$.requestBids.getHooks().remove();
resetDebugging();
sinon.stub(filters, 'isActualBid').returns(true); // stub this out so that we can use vanilla objects as bids
+ getBidToRender.before(getBidToRenderHook, 100);
// preload creative renderer
getCreativeRenderer({}).then(() => done());
});
@@ -229,6 +234,7 @@ describe('Unit: Prebid Module', function () {
after(function() {
auctionManager.clearAllAuctions();
filters.isActualBid.restore();
+ getBidToRender.getHooks({hook: getBidToRenderHook}).remove();
});
describe('and global adUnits', () => {
@@ -1246,16 +1252,25 @@ describe('Unit: Prebid Module', function () {
spyAddWinningBid.restore();
});
+ function renderAd(...args) {
+ $$PREBID_GLOBAL$$.renderAd(...args);
+ return new Promise((resolve) => {
+ setTimeout(resolve, 10);
+ });
+ }
+
it('should require doc and id params', function () {
- $$PREBID_GLOBAL$$.renderAd();
- var error = 'Error rendering ad (id: undefined): missing adId';
- assert.ok(spyLogError.calledWith(error), 'expected param error was logged');
+ return renderAd().then(() => {
+ var error = 'Error rendering ad (id: undefined): missing adId';
+ assert.ok(spyLogError.calledWith(error), 'expected param error was logged');
+ })
});
it('should log message with bid id', function () {
- $$PREBID_GLOBAL$$.renderAd(doc, bidId);
- var message = 'Calling renderAd with adId :' + bidId;
- assert.ok(spyLogMessage.calledWith(message), 'expected message was logged');
+ return renderAd(doc, bidId).then(() => {
+ var message = 'Calling renderAd with adId :' + bidId;
+ assert.ok(spyLogMessage.calledWith(message), 'expected message was logged');
+ })
});
it('should write the ad to the doc', function () {
@@ -1263,23 +1278,26 @@ describe('Unit: Prebid Module', function () {
ad: ""
});
adResponse.ad = "";
- $$PREBID_GLOBAL$$.renderAd(doc, bidId);
- assert.ok(doc.write.calledWith(adResponse.ad), 'ad was written to doc');
- assert.ok(doc.close.called, 'close method called');
+ return renderAd(doc, bidId).then(() => {
+ assert.ok(doc.write.calledWith(adResponse.ad), 'ad was written to doc');
+ assert.ok(doc.close.called, 'close method called');
+ })
});
it('should place the url inside an iframe on the doc', function () {
pushBidResponseToAuction({
adUrl: 'http://server.example.com/ad/ad.js'
});
- $$PREBID_GLOBAL$$.renderAd(doc, bidId);
- sinon.assert.calledWith(doc.createElement, 'iframe');
+ return renderAd(doc, bidId).then(() => {
+ sinon.assert.calledWith(doc.createElement, 'iframe');
+ });
});
it('should log an error when no ad or url', function () {
pushBidResponseToAuction({});
- $$PREBID_GLOBAL$$.renderAd(doc, bidId);
- sinon.assert.called(spyLogError);
+ return renderAd(doc, bidId).then(() => {
+ sinon.assert.called(spyLogError);
+ });
});
it('should log an error when not in an iFrame', function () {
@@ -1287,17 +1305,19 @@ describe('Unit: Prebid Module', function () {
ad: ""
});
inIframe = false;
- $$PREBID_GLOBAL$$.renderAd(document, bidId);
- const error = `Error rendering ad (id: ${bidId}): renderAd was prevented from writing to the main document.`;
- assert.ok(spyLogError.calledWith(error), 'expected error was logged');
+ return renderAd(document, bidId).then(() => {
+ const error = `Error rendering ad (id: ${bidId}): renderAd was prevented from writing to the main document.`;
+ assert.ok(spyLogError.calledWith(error), 'expected error was logged');
+ });
});
it('should not render videos', function () {
pushBidResponseToAuction({
mediatype: 'video'
});
- $$PREBID_GLOBAL$$.renderAd(doc, bidId);
- sinon.assert.notCalled(doc.write);
+ return renderAd(doc, bidId).then(() => {
+ sinon.assert.notCalled(doc.write);
+ });
});
it('should catch errors thrown when trying to write ads to the page', function () {
@@ -1307,25 +1327,28 @@ describe('Unit: Prebid Module', function () {
var error = { message: 'doc write error' };
doc.write = sinon.stub().throws(error);
- $$PREBID_GLOBAL$$.renderAd(doc, bidId);
- var errorMessage = `Error rendering ad (id: ${bidId}): doc write error`
- assert.ok(spyLogError.calledWith(errorMessage), 'expected error was logged');
+ return renderAd(doc, bidId).then(() => {
+ var errorMessage = `Error rendering ad (id: ${bidId}): doc write error`
+ assert.ok(spyLogError.calledWith(errorMessage), 'expected error was logged');
+ });
});
it('should log an error when ad not found', function () {
var fakeId = 99;
- $$PREBID_GLOBAL$$.renderAd(doc, fakeId);
- var error = `Error rendering ad (id: ${fakeId}): Cannot find ad '${fakeId}'`
- assert.ok(spyLogError.calledWith(error), 'expected error was logged');
+ return renderAd(doc, fakeId).then(() => {
+ var error = `Error rendering ad (id: ${fakeId}): Cannot find ad '${fakeId}'`
+ assert.ok(spyLogError.calledWith(error), 'expected error was logged');
+ });
});
it('should save bid displayed to winning bid', function () {
pushBidResponseToAuction({
ad: ""
});
- $$PREBID_GLOBAL$$.renderAd(doc, bidId);
- assert.deepEqual($$PREBID_GLOBAL$$.getAllWinningBids()[0], adResponse);
+ return renderAd(doc, bidId).then(() => {
+ assert.deepEqual($$PREBID_GLOBAL$$.getAllWinningBids()[0], adResponse);
+ });
});
it('fires billing url if present on s2s bid', function () {
@@ -1336,22 +1359,23 @@ describe('Unit: Prebid Module', function () {
burl
});
- $$PREBID_GLOBAL$$.renderAd(doc, bidId);
-
- sinon.assert.calledOnce(triggerPixelStub);
- sinon.assert.calledWith(triggerPixelStub, burl);
+ return renderAd(doc, bidId).then(() => {
+ sinon.assert.calledOnce(triggerPixelStub);
+ sinon.assert.calledWith(triggerPixelStub, burl);
+ });
});
it('should call addWinningBid', function () {
pushBidResponseToAuction({
ad: ""
});
- $$PREBID_GLOBAL$$.renderAd(doc, bidId);
- var message = 'Calling renderAd with adId :' + bidId;
- sinon.assert.calledWith(spyLogMessage, message);
+ return renderAd(doc, bidId).then(() => {
+ var message = 'Calling renderAd with adId :' + bidId;
+ sinon.assert.calledWith(spyLogMessage, message);
- sinon.assert.calledOnce(spyAddWinningBid);
- sinon.assert.calledWith(spyAddWinningBid, adResponse);
+ sinon.assert.calledOnce(spyAddWinningBid);
+ sinon.assert.calledWith(spyAddWinningBid, adResponse);
+ });
});
it('should warn stale rendering', function () {
@@ -1368,38 +1392,40 @@ describe('Unit: Prebid Module', function () {
});
// First render should pass with no warning and added to winning bids
- $$PREBID_GLOBAL$$.renderAd(doc, bidId);
- sinon.assert.calledWith(spyLogMessage, message);
- sinon.assert.neverCalledWith(spyLogWarn, warning);
+ return renderAd(doc, bidId).then(() => {
+ sinon.assert.calledWith(spyLogMessage, message);
+ sinon.assert.neverCalledWith(spyLogWarn, warning);
- sinon.assert.calledOnce(spyAddWinningBid);
- sinon.assert.calledWith(spyAddWinningBid, adResponse);
+ sinon.assert.calledOnce(spyAddWinningBid);
+ sinon.assert.calledWith(spyAddWinningBid, adResponse);
- sinon.assert.calledWith(onWonEvent, adResponse);
- sinon.assert.notCalled(onStaleEvent);
- expect(adResponse).to.have.property('status', BID_STATUS.RENDERED);
+ sinon.assert.calledWith(onWonEvent, adResponse);
+ sinon.assert.notCalled(onStaleEvent);
+ expect(adResponse).to.have.property('status', BID_STATUS.RENDERED);
- // Reset call history for spies and stubs
- spyLogMessage.resetHistory();
- spyLogWarn.resetHistory();
- spyAddWinningBid.resetHistory();
- onWonEvent.resetHistory();
- onStaleEvent.resetHistory();
+ // Reset call history for spies and stubs
+ spyLogMessage.resetHistory();
+ spyLogWarn.resetHistory();
+ spyAddWinningBid.resetHistory();
+ onWonEvent.resetHistory();
+ onStaleEvent.resetHistory();
- // Second render should have a warning but still added to winning bids
- $$PREBID_GLOBAL$$.renderAd(doc, bidId);
- sinon.assert.calledWith(spyLogMessage, message);
- sinon.assert.calledWith(spyLogWarn, warning);
+ // Second render should have a warning but still added to winning bids
+ return renderAd(doc, bidId);
+ }).then(() => {
+ sinon.assert.calledWith(spyLogMessage, message);
+ sinon.assert.calledWith(spyLogWarn, warning);
- sinon.assert.calledOnce(spyAddWinningBid);
- sinon.assert.calledWith(spyAddWinningBid, adResponse);
+ sinon.assert.calledOnce(spyAddWinningBid);
+ sinon.assert.calledWith(spyAddWinningBid, adResponse);
- sinon.assert.calledWith(onWonEvent, adResponse);
- sinon.assert.calledWith(onStaleEvent, adResponse);
+ sinon.assert.calledWith(onWonEvent, adResponse);
+ sinon.assert.calledWith(onStaleEvent, adResponse);
- // Clean up
- $$PREBID_GLOBAL$$.offEvent(EVENTS.BID_WON, onWonEvent);
- $$PREBID_GLOBAL$$.offEvent(EVENTS.STALE_RENDER, onStaleEvent);
+ // Clean up
+ $$PREBID_GLOBAL$$.offEvent(EVENTS.BID_WON, onWonEvent);
+ $$PREBID_GLOBAL$$.offEvent(EVENTS.STALE_RENDER, onStaleEvent);
+ });
});
it('should stop stale rendering', function () {
@@ -1419,38 +1445,40 @@ describe('Unit: Prebid Module', function () {
});
// First render should pass with no warning and added to winning bids
- $$PREBID_GLOBAL$$.renderAd(doc, bidId);
- sinon.assert.calledWith(spyLogMessage, message);
- sinon.assert.neverCalledWith(spyLogWarn, warning);
-
- sinon.assert.calledOnce(spyAddWinningBid);
- sinon.assert.calledWith(spyAddWinningBid, adResponse);
- expect(adResponse).to.have.property('status', BID_STATUS.RENDERED);
-
- sinon.assert.calledWith(onWonEvent, adResponse);
- sinon.assert.notCalled(onStaleEvent);
-
- // Reset call history for spies and stubs
- spyLogMessage.resetHistory();
- spyLogWarn.resetHistory();
- spyAddWinningBid.resetHistory();
- onWonEvent.resetHistory();
- onStaleEvent.resetHistory();
-
- // Second render should have a warning and do not proceed further
- $$PREBID_GLOBAL$$.renderAd(doc, bidId);
- sinon.assert.calledWith(spyLogMessage, message);
- sinon.assert.calledWith(spyLogWarn, warning);
-
- sinon.assert.notCalled(spyAddWinningBid);
-
- sinon.assert.notCalled(onWonEvent);
- sinon.assert.calledWith(onStaleEvent, adResponse);
-
- // Clean up
- $$PREBID_GLOBAL$$.offEvent(EVENTS.BID_WON, onWonEvent);
- $$PREBID_GLOBAL$$.offEvent(EVENTS.STALE_RENDER, onStaleEvent);
- configObj.setConfig({'auctionOptions': {}});
+ return renderAd(doc, bidId).then(() => {
+ sinon.assert.calledWith(spyLogMessage, message);
+ sinon.assert.neverCalledWith(spyLogWarn, warning);
+
+ sinon.assert.calledOnce(spyAddWinningBid);
+ sinon.assert.calledWith(spyAddWinningBid, adResponse);
+ expect(adResponse).to.have.property('status', BID_STATUS.RENDERED);
+
+ sinon.assert.calledWith(onWonEvent, adResponse);
+ sinon.assert.notCalled(onStaleEvent);
+
+ // Reset call history for spies and stubs
+ spyLogMessage.resetHistory();
+ spyLogWarn.resetHistory();
+ spyAddWinningBid.resetHistory();
+ onWonEvent.resetHistory();
+ onStaleEvent.resetHistory();
+
+ // Second render should have a warning and do not proceed further
+ return renderAd(doc, bidId);
+ }).then(() => {
+ sinon.assert.calledWith(spyLogMessage, message);
+ sinon.assert.calledWith(spyLogWarn, warning);
+
+ sinon.assert.notCalled(spyAddWinningBid);
+
+ sinon.assert.notCalled(onWonEvent);
+ sinon.assert.calledWith(onStaleEvent, adResponse);
+
+ // Clean up
+ $$PREBID_GLOBAL$$.offEvent(EVENTS.BID_WON, onWonEvent);
+ $$PREBID_GLOBAL$$.offEvent(EVENTS.STALE_RENDER, onStaleEvent);
+ configObj.setConfig({'auctionOptions': {}});
+ });
});
});
diff --git a/test/spec/unit/secureCreatives_spec.js b/test/spec/unit/secureCreatives_spec.js
index 189066f7f889..664ba51ff1fd 100644
--- a/test/spec/unit/secureCreatives_spec.js
+++ b/test/spec/unit/secureCreatives_spec.js
@@ -13,11 +13,23 @@ import 'modules/nativeRendering.js';
import {expect} from 'chai';
-import { AD_RENDER_FAILED_REASON, BID_STATUS, EVENTS } from 'src/constants.js';
+import {AD_RENDER_FAILED_REASON, BID_STATUS, EVENTS} from 'src/constants.js';
+import {getBidToRender} from '../../../src/adRendering.js';
describe('secureCreatives', () => {
let sandbox;
+ function getBidToRenderHook(next, adId) {
+ // make sure that bids can be retrieved asynchronously
+ next(adId, new Promise((resolve) => setTimeout(resolve)))
+ }
+ before(() => {
+ getBidToRender.before(getBidToRenderHook);
+ });
+ after(() => {
+ getBidToRender.getHooks({hook: getBidToRenderHook}).remove()
+ });
+
beforeEach(() => {
sandbox = sinon.sandbox.create();
});
@@ -30,6 +42,10 @@ describe('secureCreatives', () => {
return Object.assign({origin: 'mock-origin', ports: []}, ev)
}
+ function receive(ev) {
+ return Promise.resolve(receiveMessage(ev));
+ }
+
describe('getReplier', () => {
it('should use source.postMessage if no MessagePort is available', () => {
const ev = {
@@ -153,17 +169,17 @@ describe('secureCreatives', () => {
data: JSON.stringify(data),
});
- receiveMessage(ev);
-
- sinon.assert.neverCalledWith(spyLogWarn, warning);
- sinon.assert.calledOnce(spyAddWinningBid);
- sinon.assert.calledWith(spyAddWinningBid, adResponse);
- sinon.assert.calledOnce(adResponse.renderer.render);
- sinon.assert.calledWith(adResponse.renderer.render, adResponse);
- sinon.assert.calledWith(stubEmit, EVENTS.BID_WON, adResponse);
- sinon.assert.neverCalledWith(stubEmit, EVENTS.STALE_RENDER);
+ return receive(ev).then(() => {
+ sinon.assert.neverCalledWith(spyLogWarn, warning);
+ sinon.assert.calledOnce(spyAddWinningBid);
+ sinon.assert.calledWith(spyAddWinningBid, adResponse);
+ sinon.assert.calledOnce(adResponse.renderer.render);
+ sinon.assert.calledWith(adResponse.renderer.render, adResponse);
+ sinon.assert.calledWith(stubEmit, EVENTS.BID_WON, adResponse);
+ sinon.assert.neverCalledWith(stubEmit, EVENTS.STALE_RENDER);
- expect(adResponse).to.have.property('status', BID_STATUS.RENDERED);
+ expect(adResponse).to.have.property('status', BID_STATUS.RENDERED);
+ });
});
it('should allow stale rendering without config', function () {
@@ -180,29 +196,26 @@ describe('secureCreatives', () => {
data: JSON.stringify(data)
});
- receiveMessage(ev);
-
- sinon.assert.neverCalledWith(spyLogWarn, warning);
- sinon.assert.calledOnce(spyAddWinningBid);
- sinon.assert.calledWith(spyAddWinningBid, adResponse);
- sinon.assert.calledOnce(adResponse.renderer.render);
- sinon.assert.calledWith(adResponse.renderer.render, adResponse);
- sinon.assert.calledWith(stubEmit, EVENTS.BID_WON, adResponse);
- sinon.assert.neverCalledWith(stubEmit, EVENTS.STALE_RENDER);
-
- expect(adResponse).to.have.property('status', BID_STATUS.RENDERED);
-
- resetHistories(adResponse.renderer.render);
-
- receiveMessage(ev);
-
- sinon.assert.calledWith(spyLogWarn, warning);
- sinon.assert.calledOnce(spyAddWinningBid);
- sinon.assert.calledWith(spyAddWinningBid, adResponse);
- sinon.assert.calledOnce(adResponse.renderer.render);
- sinon.assert.calledWith(adResponse.renderer.render, adResponse);
- sinon.assert.calledWith(stubEmit, EVENTS.BID_WON, adResponse);
- sinon.assert.calledWith(stubEmit, EVENTS.STALE_RENDER, adResponse);
+ return receive(ev).then(() => {
+ sinon.assert.neverCalledWith(spyLogWarn, warning);
+ sinon.assert.calledOnce(spyAddWinningBid);
+ sinon.assert.calledWith(spyAddWinningBid, adResponse);
+ sinon.assert.calledOnce(adResponse.renderer.render);
+ sinon.assert.calledWith(adResponse.renderer.render, adResponse);
+ sinon.assert.calledWith(stubEmit, EVENTS.BID_WON, adResponse);
+ sinon.assert.neverCalledWith(stubEmit, EVENTS.STALE_RENDER);
+ expect(adResponse).to.have.property('status', BID_STATUS.RENDERED);
+ resetHistories(adResponse.renderer.render);
+ return receive(ev);
+ }).then(() => {
+ sinon.assert.calledWith(spyLogWarn, warning);
+ sinon.assert.calledOnce(spyAddWinningBid);
+ sinon.assert.calledWith(spyAddWinningBid, adResponse);
+ sinon.assert.calledOnce(adResponse.renderer.render);
+ sinon.assert.calledWith(adResponse.renderer.render, adResponse);
+ sinon.assert.calledWith(stubEmit, EVENTS.BID_WON, adResponse);
+ sinon.assert.calledWith(stubEmit, EVENTS.STALE_RENDER, adResponse);
+ });
});
it('should stop stale rendering with config', function () {
@@ -221,29 +234,27 @@ describe('secureCreatives', () => {
data: JSON.stringify(data)
});
- receiveMessage(ev);
-
- sinon.assert.neverCalledWith(spyLogWarn, warning);
- sinon.assert.calledOnce(spyAddWinningBid);
- sinon.assert.calledWith(spyAddWinningBid, adResponse);
- sinon.assert.calledOnce(adResponse.renderer.render);
- sinon.assert.calledWith(adResponse.renderer.render, adResponse);
- sinon.assert.calledWith(stubEmit, EVENTS.BID_WON, adResponse);
- sinon.assert.neverCalledWith(stubEmit, EVENTS.STALE_RENDER);
-
- expect(adResponse).to.have.property('status', BID_STATUS.RENDERED);
-
- resetHistories(adResponse.renderer.render);
-
- receiveMessage(ev);
-
- sinon.assert.calledWith(spyLogWarn, warning);
- sinon.assert.notCalled(spyAddWinningBid);
- sinon.assert.notCalled(adResponse.renderer.render);
- sinon.assert.neverCalledWith(stubEmit, EVENTS.BID_WON, adResponse);
- sinon.assert.calledWith(stubEmit, EVENTS.STALE_RENDER, adResponse);
-
- configObj.setConfig({'auctionOptions': {}});
+ return receive(ev).then(() => {
+ sinon.assert.neverCalledWith(spyLogWarn, warning);
+ sinon.assert.calledOnce(spyAddWinningBid);
+ sinon.assert.calledWith(spyAddWinningBid, adResponse);
+ sinon.assert.calledOnce(adResponse.renderer.render);
+ sinon.assert.calledWith(adResponse.renderer.render, adResponse);
+ sinon.assert.calledWith(stubEmit, EVENTS.BID_WON, adResponse);
+ sinon.assert.neverCalledWith(stubEmit, EVENTS.STALE_RENDER);
+
+ expect(adResponse).to.have.property('status', BID_STATUS.RENDERED);
+
+ resetHistories(adResponse.renderer.render);
+ return receive(ev)
+ }).then(() => {
+ sinon.assert.calledWith(spyLogWarn, warning);
+ sinon.assert.notCalled(spyAddWinningBid);
+ sinon.assert.notCalled(adResponse.renderer.render);
+ sinon.assert.neverCalledWith(stubEmit, EVENTS.BID_WON, adResponse);
+ sinon.assert.calledWith(stubEmit, EVENTS.STALE_RENDER, adResponse);
+ configObj.setConfig({'auctionOptions': {}});
+ });
});
it('should emit AD_RENDER_FAILED if requested missing adId', () => {
@@ -253,11 +264,12 @@ describe('secureCreatives', () => {
adId: 'missing'
})
});
- receiveMessage(ev);
- sinon.assert.calledWith(stubEmit, EVENTS.AD_RENDER_FAILED, sinon.match({
- reason: AD_RENDER_FAILED_REASON.CANNOT_FIND_AD,
- adId: 'missing'
- }));
+ return receive(ev).then(() => {
+ sinon.assert.calledWith(stubEmit, EVENTS.AD_RENDER_FAILED, sinon.match({
+ reason: AD_RENDER_FAILED_REASON.CANNOT_FIND_AD,
+ adId: 'missing'
+ }));
+ });
});
it('should emit AD_RENDER_FAILED if creative can\'t be sent to rendering frame', () => {
@@ -271,11 +283,12 @@ describe('secureCreatives', () => {
adId: bidId
})
});
- receiveMessage(ev)
- sinon.assert.calledWith(stubEmit, EVENTS.AD_RENDER_FAILED, sinon.match({
- reason: AD_RENDER_FAILED_REASON.EXCEPTION,
- adId: bidId
- }));
+ return receive(ev).then(() => {
+ sinon.assert.calledWith(stubEmit, EVENTS.AD_RENDER_FAILED, sinon.match({
+ reason: AD_RENDER_FAILED_REASON.EXCEPTION,
+ adId: bidId
+ }));
+ })
});
it('should include renderers in responses', () => {
@@ -287,8 +300,9 @@ describe('secureCreatives', () => {
},
data: JSON.stringify({adId: bidId, message: 'Prebid Request'})
});
- receiveMessage(ev);
- sinon.assert.calledWith(ev.source.postMessage, sinon.match(ob => JSON.parse(ob).renderer === 'mock-renderer'));
+ return receive(ev).then(() => {
+ sinon.assert.calledWith(ev.source.postMessage, sinon.match(ob => JSON.parse(ob).renderer === 'mock-renderer'));
+ });
});
if (FEATURES.NATIVE) {
@@ -318,23 +332,24 @@ describe('secureCreatives', () => {
},
data: JSON.stringify({adId: bidId, message: 'Prebid Request'})
})
- receiveMessage(ev);
- sinon.assert.calledWith(ev.source.postMessage, sinon.match(ob => {
- const data = JSON.parse(ob);
- ['width', 'height'].forEach(prop => expect(data[prop]).to.not.exist);
- const native = data.native;
- sinon.assert.match(native, {
- ortb: bid.native.ortb,
- adTemplate: bid.native.adTemplate,
- rendererUrl: bid.native.rendererUrl,
- })
- expect(Object.fromEntries(native.assets.map(({key, value}) => [key, value]))).to.eql({
- adTemplate: bid.native.adTemplate,
- rendererUrl: bid.native.rendererUrl,
- body: 'vbody'
- });
- return true;
- }))
+ return receive(ev).then(() => {
+ sinon.assert.calledWith(ev.source.postMessage, sinon.match(ob => {
+ const data = JSON.parse(ob);
+ ['width', 'height'].forEach(prop => expect(data[prop]).to.not.exist);
+ const native = data.native;
+ sinon.assert.match(native, {
+ ortb: bid.native.ortb,
+ adTemplate: bid.native.adTemplate,
+ rendererUrl: bid.native.rendererUrl,
+ })
+ expect(Object.fromEntries(native.assets.map(({key, value}) => [key, value]))).to.eql({
+ adTemplate: bid.native.adTemplate,
+ rendererUrl: bid.native.rendererUrl,
+ body: 'vbody'
+ });
+ return true;
+ }))
+ });
})
}
});
@@ -361,16 +376,16 @@ describe('secureCreatives', () => {
origin: 'any origin'
});
- receiveMessage(ev);
-
- sinon.assert.neverCalledWith(spyLogWarn, warning);
- sinon.assert.calledOnce(stubGetAllAssetsMessage);
- sinon.assert.calledWith(stubGetAllAssetsMessage, data, adResponse);
- sinon.assert.calledOnce(ev.source.postMessage);
- sinon.assert.notCalled(stubFireNativeTrackers);
- sinon.assert.calledWith(stubEmit, EVENTS.BID_WON, adResponse);
- sinon.assert.calledOnce(spyAddWinningBid);
- sinon.assert.neverCalledWith(stubEmit, EVENTS.STALE_RENDER);
+ return receive(ev).then(() => {
+ sinon.assert.neverCalledWith(spyLogWarn, warning);
+ sinon.assert.calledOnce(stubGetAllAssetsMessage);
+ sinon.assert.calledWith(stubGetAllAssetsMessage, data, adResponse);
+ sinon.assert.calledOnce(ev.source.postMessage);
+ sinon.assert.notCalled(stubFireNativeTrackers);
+ sinon.assert.calledWith(stubEmit, EVENTS.BID_WON, adResponse);
+ sinon.assert.calledOnce(spyAddWinningBid);
+ sinon.assert.neverCalledWith(stubEmit, EVENTS.STALE_RENDER);
+ });
});
it('Prebid native should not fire BID_WON when receiveMessage is called more than once', () => {
@@ -391,11 +406,12 @@ describe('secureCreatives', () => {
origin: 'any origin'
});
- receiveMessage(ev);
- sinon.assert.calledWith(stubEmit, EVENTS.BID_WON, adResponse);
-
- receiveMessage(ev);
- stubEmit.withArgs(EVENTS.BID_WON, adResponse).calledOnce;
+ return receive(ev).then(() => {
+ sinon.assert.calledWith(stubEmit, EVENTS.BID_WON, adResponse);
+ return receive(ev);
+ }).then(() => {
+ stubEmit.withArgs(EVENTS.BID_WON, adResponse).calledOnce;
+ });
});
});
@@ -422,13 +438,14 @@ describe('secureCreatives', () => {
},
})
});
- receiveMessage(event);
- expect(stubEmit.calledWith(EVENTS.AD_RENDER_FAILED, {
- adId: bidId,
- bid: adResponse,
- reason: 'Fail reason',
- message: 'Fail message'
- })).to.equal(shouldEmit);
+ return receive(event).then(() => {
+ expect(stubEmit.calledWith(EVENTS.AD_RENDER_FAILED, {
+ adId: bidId,
+ bid: adResponse,
+ reason: 'Fail reason',
+ message: 'Fail message'
+ })).to.equal(shouldEmit);
+ });
});
it(`should${shouldEmit ? ' ' : ' not '}emit AD_RENDER_SUCCEEDED`, () => {
@@ -439,12 +456,13 @@ describe('secureCreatives', () => {
adId: bidId,
})
});
- receiveMessage(event);
- expect(stubEmit.calledWith(EVENTS.AD_RENDER_SUCCEEDED, {
- adId: bidId,
- bid: adResponse,
- doc: null
- })).to.equal(shouldEmit);
+ return receive(event).then(() => {
+ expect(stubEmit.calledWith(EVENTS.AD_RENDER_SUCCEEDED, {
+ adId: bidId,
+ bid: adResponse,
+ doc: null
+ })).to.equal(shouldEmit);
+ });
});
});
});
diff --git a/test/spec/utils_spec.js b/test/spec/utils_spec.js
index fb85b410bd94..d7d7007674a1 100644
--- a/test/spec/utils_spec.js
+++ b/test/spec/utils_spec.js
@@ -1,9 +1,8 @@
import {getAdServerTargeting} from 'test/fixtures/fixtures.js';
import {expect} from 'chai';
-import { TARGETING_KEYS } from 'src/constants.js';
+import {TARGETING_KEYS} from 'src/constants.js';
import * as utils from 'src/utils.js';
-import {getHighestCpm, getLatestHighestCpmBid, getOldestHighestCpmBid} from '../../src/utils/reducers.js';
-import {binarySearch, deepEqual, encodeMacroURI, memoize, waitForElementToLoad} from 'src/utils.js';
+import {binarySearch, deepEqual, encodeMacroURI, memoize, sizesToSizeTuples, waitForElementToLoad} from 'src/utils.js';
import {convertCamelToUnderscore} from '../../libraries/appnexusUtils/anUtils.js';
var assert = require('assert');
@@ -167,6 +166,43 @@ describe('Utils', function () {
});
});
+ describe('sizesToSizeTuples', () => {
+ Object.entries({
+ 'single size, numerical': {
+ in: [1, 2],
+ out: [[1, 2]]
+ },
+ 'single size, numerical, nested': {
+ in: [[1, 2]],
+ out: [[1, 2]]
+ },
+ 'multiple sizes, numerical': {
+ in: [[1, 2], [3, 4]],
+ out: [[1, 2], [3, 4]]
+ },
+ 'single size, string': {
+ in: '1x2',
+ out: [[1, 2]]
+ },
+ 'multiple sizes, string': {
+ in: '1x2, 4x3',
+ out: [[1, 2], [4, 3]]
+ },
+ 'incorrect size, numerical': {
+ in: [1],
+ out: []
+ },
+ 'incorrect size, string': {
+ in: '1x',
+ out: []
+ }
+ }).forEach(([t, {in: input, out}]) => {
+ it(`can parse ${t}`, () => {
+ expect(sizesToSizeTuples(input)).to.eql(out);
+ })
+ })
+ })
+
describe('parseSizesInput', function () {
it('should return query string using multi size array', function () {
var sizes = [[728, 90], [970, 90]];