From 82ea027c38849653fca4c2d3fee4a9ffa7a3f517 Mon Sep 17 00:00:00 2001 From: Alexander Clouter Date: Tue, 10 Jan 2023 18:01:25 +0000 Subject: [PATCH] Adloox RTD Module: fix breakage since 7.x release (#9383) getTargetingData resulted in a crash as 'auction' is no longer passed in and we needed access to the global ORTB2 targetings. Reworked to be a lot simplier, and removed the ATF viewability segment and use the results of intersectionRtdProvider if present --- integrationExamples/gpt/adloox.html | 5 + modules/adlooxAnalyticsAdapter.md | 8 +- modules/adlooxRtdProvider.js | 360 +++++--------------- modules/adlooxRtdProvider.md | 12 +- test/spec/modules/adlooxRtdProvider_spec.js | 148 ++------ 5 files changed, 126 insertions(+), 407 deletions(-) diff --git a/integrationExamples/gpt/adloox.html b/integrationExamples/gpt/adloox.html index fd61267479d..78fba71f774 100644 --- a/integrationExamples/gpt/adloox.html +++ b/integrationExamples/gpt/adloox.html @@ -147,8 +147,13 @@ realTimeData: { auctionDelay: AUCTION_DELAY, dataProviders: [ + { + name: 'intersection', + waitForIt: true + }, { name: 'adloox', + waitForIt: true, params: { // optional, defaults shown thresholds: [ 50, 60, 70, 80, 90 ], slotinpath: false diff --git a/modules/adlooxAnalyticsAdapter.md b/modules/adlooxAnalyticsAdapter.md index 203b118652e..d77ee25ab5f 100644 --- a/modules/adlooxAnalyticsAdapter.md +++ b/modules/adlooxAnalyticsAdapter.md @@ -34,19 +34,21 @@ When tracking video you have two options: To view an [example of an Adloox integration](../integrationExamples/gpt/adloox.html): - gulp serve --nolint --notest --modules=gptPreAuction,categoryTranslation,dfpAdServerVideo,rtdModule,instreamTracking,rubiconBidAdapter,spotxBidAdapter,adlooxAnalyticsAdapter,adlooxAdServerVideo,adlooxRtdProvider + gulp serve --nolint --notest --modules=gptPreAuction,categoryTranslation,dfpAdServerVideo,intersectionRtdProvider,rtdModule,instreamTracking,rubiconBidAdapter,spotxBidAdapter,adlooxAnalyticsAdapter,adlooxAdServerVideo,adlooxRtdProvider **N.B.** `categoryTranslation` is required by `dfpAdServerVideo` that otherwise causes a JavaScript console warning +**N.B.** `intersectionRtdProvider` is used by `adlooxRtdProvider` to provide (above-the-fold) ATF measurement, if not enabled the `atf` segment will not be available + Now point your browser at: http://localhost:9999/integrationExamples/gpt/adloox.html?pbjs_debug=true ### Public Example -The example is published publically at: https://storage.googleapis.com/adloox-ads-js-test/prebid.html?pbjs_debug=true +The example is published publicly at: https://storage.googleapis.com/adloox-ads-js-test/prebid.html?pbjs_debug=true **N.B.** this will show a [CORS error](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS/Errors) for the request `https://p.adlooxtracking.com/q?...` that is safe to ignore on the public example page; it is related to the [RTD integration](./adlooxRtdProvider.md) which requires pre-registration of your sites -It is recommended you use [Google Chrome's 'Local Overrides' located in the Developer Tools panel](https://www.trysmudford.com/blog/chrome-local-overrides/) to explore the example without the inconvience of having to run your own web server. +It is recommended you use [Google Chrome's 'Local Overrides' located in the Developer Tools panel](https://www.trysmudford.com/blog/chrome-local-overrides/) to explore the example without the inconvenience of having to run your own web server. #### Pre-built `prebid.js` diff --git a/modules/adlooxRtdProvider.js b/modules/adlooxRtdProvider.js index 8862ac8ac47..c2037429185 100644 --- a/modules/adlooxRtdProvider.js +++ b/modules/adlooxRtdProvider.js @@ -6,11 +6,13 @@ * @module modules/adlooxRtdProvider * @requires module:modules/realTimeData * @requires module:modules/adlooxAnalyticsAdapter + * @optional module:modules/intersectionRtdProvider */ /* eslint standard/no-callback-literal: "off" */ /* eslint prebid/validate-imports: "off" */ +import {auctionManager} from '../src/auctionManager.js'; import {command as analyticsCommand, COMMAND} from './adlooxAnalyticsAdapter.js'; import {submodule} from '../src/hook.js'; import {ajax} from '../src/ajax.js'; @@ -18,142 +20,31 @@ import {getGlobal} from '../src/prebidGlobal.js'; import {getRefererInfo} from '../src/refererDetection.js'; import { _each, + _map, + buildUrl, deepAccess, + deepClone, deepSetValue, - getAdUnitSizes, getGptSlotInfoForAdUnitCode, isArray, isBoolean, isInteger, isPlainObject, - isStr, logError, logInfo, logWarn, - mergeDeep + mergeDeep, + parseUrl, + safeJSONParse } from '../src/utils.js'; -import {includes} from '../src/polyfill.js'; const MODULE_NAME = 'adloox'; const MODULE = `${MODULE_NAME}RtdProvider`; const API_ORIGIN = 'https://p.adlooxtracking.com'; const SEGMENT_HISTORIC = { 'a': 'aud', 'd': 'dis', 'v': 'vid' }; -const SEGMENT_HISTORIC_VALUES = Object.keys(SEGMENT_HISTORIC).map(k => SEGMENT_HISTORIC[k]); -const ADSERVER_TARGETING_PREFIX = 'adl_'; - -const CREATIVE_WIDTH_MIN = 20; -const CREATIVE_HEIGHT_MIN = 20; -const CREATIVE_AREA_MIN = CREATIVE_WIDTH_MIN * CREATIVE_HEIGHT_MIN; -// try to avoid using IntersectionObserver as it has unbounded (observed multi-second) latency -let intersectionObserver = window == top ? false : undefined; -const intersectionObserverElements = []; -// .map/.findIndex are safe here as they are only used for intersectionObserver -function atf(adUnit, cb) { - analyticsCommand(COMMAND.TOSELECTOR, { bid: { adUnitCode: adUnit.code } }, function(selector) { - const element = document.querySelector(selector); - if (!element) return cb(null); - - if (window.getComputedStyle(element)['display'] == 'none') return cb(NaN); - - try { - // short circuit for cross-origin - if (intersectionObserver) throw false; - - // Google's advice is to collapse slots on no fill but - // we have to cater to clients that grow slots on fill - const rect = (function(rect) { - const sizes = getAdUnitSizes(adUnit); - - if (sizes.length == 0) return false; - // interstitial (0x0, 1x1) - if (sizes.length == 1 && (sizes[0][0] * sizes[0][1]) <= 1) return true; - // try to catch premium slots (coord=0,0) as they will likely bounce into view - if (rect.top <= -window.pageYOffset && rect.left <= -window.pageXOffset && rect.top == rect.bottom) return true; - - // pick the smallest creative size as many publishers will just leave the element unbounded in the vertical - let width = Infinity; - let height = Infinity; - for (let i = 0; i < sizes.length; i++) { - const area = sizes[i][0] * sizes[i][1]; - if (area < CREATIVE_AREA_MIN) continue; - if (area < (width * height)) { - width = sizes[i][0]; - height = sizes[i][1]; - } - } - // we also scale the smallest size to the size of the slot as publishers resize units depending on viewport - const scale = Math.min(1, (rect.right - rect.left) / width); - - return { - left: rect.left, - right: rect.left + Math.max(CREATIVE_WIDTH_MIN, scale * width), - top: rect.top, - bottom: rect.top + Math.max(CREATIVE_HEIGHT_MIN, scale * height) - }; - })(element.getBoundingClientRect()); - - if (rect === false) return cb(NaN); - if (rect === true) return cb(1); - - const W = rect.right - rect.left; - const H = rect.bottom - rect.top; - - if (W * H < CREATIVE_AREA_MIN) return cb(NaN); - - let el; - let win = window; - while (1) { - // https://stackoverflow.com/a/8876069 - const vw = Math.max(win.document.documentElement.clientWidth || 0, win.innerWidth || 0); - const vh = Math.max(win.document.documentElement.clientHeight || 0, win.innerHeight || 0); - - // cut to viewport - rect.left = Math.min(Math.max(rect.left, 0), vw); - rect.right = Math.min(Math.max(rect.right, 0), vw); - rect.top = Math.min(Math.max(rect.top, 0), vh); - rect.bottom = Math.min(Math.max(rect.bottom, 0), vh); - - if (win == top) return cb(((rect.right - rect.left) * (rect.bottom - rect.top)) / (W * H)); - el = win.frameElement; - if (!el) throw false; // cross-origin - win = win.parent; - - // transpose to frame element - const frameElementRect = el.getBoundingClientRect(); - rect.left += frameElementRect.left; - rect.right = Math.min(rect.right + frameElementRect.left, frameElementRect.right); - rect.top += frameElementRect.top; - rect.bottom = Math.min(rect.bottom + frameElementRect.top, frameElementRect.bottom); - } - } catch (_) { - if (intersectionObserver === undefined) { - try { - intersectionObserver = new IntersectionObserver(function(entries) { - entries.forEach(entry => { - const ratio = entry.intersectionRect.width * entry.intersectionRect.height < CREATIVE_AREA_MIN ? NaN : entry.intersectionRatio; - const idx = intersectionObserverElements.findIndex(x => x.element == entry.target); - intersectionObserverElements[idx].cb.forEach(cb => cb(ratio)); - intersectionObserverElements.splice(idx, 1); - intersectionObserver.unobserve(entry.target); - }); - }); - } catch (_) { - intersectionObserver = false; - } - } - if (!intersectionObserver) return cb(null); - const idx = intersectionObserverElements.findIndex(x => x.element == element); - if (idx == -1) { - intersectionObserverElements.push({ element, cb: [ cb ] }); - intersectionObserver.observe(element); - } else { - intersectionObserverElements[idx].cb.push(cb); - } - } - }); -} +const ADSERVER_TARGETING_PREFIX = 'adl'; function init(config, userConsent) { logInfo(MODULE, 'init', config, userConsent); @@ -167,10 +58,6 @@ function init(config, userConsent) { logError(MODULE, 'invalid params'); return false; } - if (!(config.params.api_origin === undefined || isStr(config.params.api_origin))) { - logError(MODULE, 'invalid api_origin params value'); - return false; - } if (!(config.params.imps === undefined || (isInteger(config.params.imps) && config.params.imps > 0))) { logError(MODULE, 'invalid imps params value'); return false; @@ -213,193 +100,110 @@ function init(config, userConsent) { } function getBidRequestData(reqBidsConfigObj, callback, config, userConsent) { - // gptPreAuction runs *after* RTD so pbadslot may not be populated... (╯°□°)╯ ┻━┻ - const adUnits = (reqBidsConfigObj.adUnits || getGlobal().adUnits).map(adUnit => { - return { - gpid: deepAccess(adUnit, 'ortb2Imp.ext.gpid') || deepAccess(adUnit, 'ortb2Imp.ext.data.pbadslot') || getGptSlotInfoForAdUnitCode(adUnit.code).gptSlot || adUnit.code, - unit: adUnit - }; - }).filter(adUnit => !!adUnit.gpid); - - let response = {}; - function setSegments() { - function val(v, k) { - if (!((SEGMENT_HISTORIC[k] || k == 'atf') && v >= 0)) return v; - return config.params.thresholds.filter(t => t <= v); - } - - const ortb2 = reqBidsConfigObj.ortb2Fragments?.global || {}; - const dataSite = deepAccess(ortb2, 'site.ext.data') || {}; - const dataUser = deepAccess(ortb2, 'user.ext.data') || {}; - - _each(response, (v0, k0) => { - if (k0 == '_') return; - const k = SEGMENT_HISTORIC[k0] || k0; - const v = val(v0, k0); - deepSetValue(k == k0 ? dataUser : dataSite, `${MODULE_NAME}_rtd.${k}`, v); - }); - deepSetValue(dataSite, `${MODULE_NAME}_rtd.ok`, true); + const adUnits0 = reqBidsConfigObj.adUnits || getGlobal().adUnits; + // adUnits must be ordered according to adUnitCodes for stable 's' param usage and handling the response below + const adUnits = reqBidsConfigObj.adUnitCodes.map(code => adUnits0.find(unit => unit.code == code)); + + // buildUrl creates PHP style multi-parameters and includes undefined... (╯°□°)╯ ┻━┻ + const url = buildUrl(mergeDeep(parseUrl(`${API_ORIGIN}/q`), { search: { + 'v': `pbjs-${getGlobal().version}`, + 'c': config.params.clientid, + 'p': config.params.platformid, + 't': config.params.tagid, + 'imp': config.params.imps, + 'fc_ip': config.params.freqcap_ip, + 'fc_ipua': config.params.freqcap_ipua, + 'pn': (getRefererInfo().page || '').substr(0, 300).split(/[?#]/)[0], + 's': _map(adUnits, function(unit) { + // gptPreAuction runs *after* RTD so pbadslot may not be populated... (╯°□°)╯ ┻━┻ + const gpid = deepAccess(unit, 'ortb2Imp.ext.gpid') || + deepAccess(unit, 'ortb2Imp.ext.data.pbadslot') || + getGptSlotInfoForAdUnitCode(unit.code).gptSlot || + unit.code; + const ref = [ gpid ]; + if (!config.params.slotinpath) ref.push(unit.code); + return ref.join('\t'); + }) + } })).replace(/\[\]|[^?&]+=undefined/g, '').replace(/([?&])&+/g, '$1'); + + ajax(url, + function(responseText, q) { + function val(v, k) { + if (!(SEGMENT_HISTORIC[k] && v >= 0)) return v; + return config.params.thresholds.filter(t => t <= v); + } - deepSetValue(ortb2, 'site.ext.data', dataSite); - deepSetValue(ortb2, 'user.ext.data', dataUser); - deepSetValue(reqBidsConfigObj, 'ortb2Fragments.global', ortb2); + const response = safeJSONParse(responseText); + if (!response) { + logError(MODULE, 'unexpected response'); + return callback(); + } - adUnits.forEach((adUnit, i) => { - _each(response['_'][i], (v0, k0) => { + const { site: ortb2site, user: ortb2user } = reqBidsConfigObj.ortb2Fragments.global; + _each(response, function(v0, k0) { + if (k0 == '_') return; const k = SEGMENT_HISTORIC[k0] || k0; const v = val(v0, k0); - deepSetValue(adUnit.unit, `ortb2Imp.ext.data.${MODULE_NAME}_rtd.${k}`, v); + deepSetValue(k == k0 ? ortb2user : ortb2site, `ext.data.${MODULE_NAME}_rtd.${k}`, v); }); - }); - }; - - // mergeDeep does not handle merging deep arrays... (╯°□°)╯ ┻━┻ - function mergeDeep(target, ...sources) { - function emptyValue(v) { - if (isPlainObject(v)) { - return {}; - } else if (isArray(v)) { - return []; - } else { - return undefined; - } - } - if (!sources.length) return target; - const source = sources.shift(); - - if (isPlainObject(target) && isPlainObject(source)) { - Object.keys(source).forEach(key => { - if (!(key in target)) target[key] = emptyValue(source[key]); - target[key] = target[key] !== undefined ? mergeDeep(target[key], source[key]) : source[key]; - }); - } else if (isArray(target) && isArray(source)) { - source.forEach((v, i) => { - if (!(i in target)) target[i] = emptyValue(v); - target[i] = target[i] !== undefined ? mergeDeep(target[i], v) : v; + _each(response._, function(segments, i) { + _each(segments, function(v0, k0) { + const k = SEGMENT_HISTORIC[k0] || k0; + const v = val(v0, k0); + deepSetValue(adUnits[i], `ortb2Imp.ext.data.${MODULE_NAME}_rtd.${k}`, v); + }); }); - } else { - target = source; - } - return mergeDeep(target, ...sources); - } + deepSetValue(ortb2site, `ext.data.${MODULE_NAME}_rtd.ok`, true); - let semaphore = 1; - function semaphoreInc(inc) { - if (semaphore == 0) return; - semaphore += inc; - if (semaphore == 0) { - setSegments() callback(); } - } - - const refererInfo = getRefererInfo(); - const args = [ - [ 'v', `pbjs-${getGlobal().version}` ], - [ 'c', config.params.clientid ], - [ 'p', config.params.platformid ], - [ 't', config.params.tagid ], - [ 'imp', config.params.imps ], - [ 'fc_ip', config.params.freqcap_ip ], - [ 'fc_ipua', config.params.freqcap_ipua ], - [ 'pn', (refererInfo.page || '').substr(0, 300).split(/[?#]/)[0] ] - ]; - - if (!adUnits.length) { - logWarn(MODULE, 'no suitable adUnits (missing pbadslot?)'); - } - const atfQueue = []; - adUnits.map((adUnit, i) => { - const ref = [ adUnit.gpid ]; - if (!config.params.slotinpath) ref.push(adUnit.unit.code); - args.push(['s', ref.join('\t')]); - - semaphoreInc(1); - atfQueue.push(function() { - atf(adUnit.unit, function(x) { - let viewable = document.visibilityState === undefined || document.visibilityState == 'visible'; - try { viewable = viewable && top.document.hasFocus() } catch (_) {} - logInfo(MODULE, `atf code=${adUnit.unit.code} has area=${x}, viewable=${viewable}`); - const atfList = []; atfList[i] = { atf: parseInt(x * 100) }; - response = mergeDeep(response, { _: atfList }); - semaphoreInc(-1); - }); - }); - }); - function atfCb() { - atfQueue.forEach(x => x()); - } - if (document.readyState == 'loading') { - document.addEventListener('DOMContentLoaded', atfCb, false); - } else { - atfCb(); - } - - analyticsCommand(COMMAND.URL, { - url: (config.params.api_origin || API_ORIGIN) + '/q?', - args: args - }, function(url) { - ajax(url, { - success: function(responseText, q) { - try { - if (q.getResponseHeader('content-type') == 'application/json') { - response = mergeDeep(response, JSON.parse(responseText)); - } else { - throw false; - } - } catch (_) { - logError(MODULE, 'unexpected response'); - } - semaphoreInc(-1); - }, - error: function(statusText, q) { - logError(MODULE, 'request failed'); - semaphoreInc(-1); - } - }); - }); + ); } function getTargetingData(adUnitArray, config, userConsent, auction) { - function targetingNormalise(v) { + function val(v) { if (isArray(v) && v.length == 0) return undefined; if (isBoolean(v)) v = ~~v; if (!v) return undefined; // empty string and zero return v; } - const ortb2 = auction.getFPD().global || {}; - const dataSite = deepAccess(ortb2, `site.ext.data.${MODULE_NAME}_rtd`) || {}; - if (!dataSite.ok) return {}; + const { site: ortb2site, user: ortb2user } = auctionManager.index.getAuction(auction).getFPD().global; - const dataUser = deepAccess(ortb2, `user.ext.data.${MODULE_NAME}_rtd`) || {}; - return getGlobal().adUnits.filter(adUnit => includes(adUnitArray, adUnit.code)).reduce((a, adUnit) => { - a[adUnit.code] = {}; + const ortb2base = {}; + _each(deepAccess(mergeDeep(ortb2site, ortb2user), `ext.data.${MODULE_NAME}_rtd`), function(v0, k) { + const v = val(v0); + if (v) ortb2base[`${ADSERVER_TARGETING_PREFIX}_${k}`] = v; + }); - _each(dataSite, (v0, k) => { - if (includes(SEGMENT_HISTORIC_VALUES, k)) return; // ignore site average viewability - const v = targetingNormalise(v0); - if (v) a[adUnit.code][ADSERVER_TARGETING_PREFIX + k] = v; - }); + const targeting = {}; + _each(auction.adUnits.filter(unit => adUnitArray.includes(unit.code)), function(unit) { + targeting[unit.code] = deepClone(ortb2base); - const adUnitSegments = deepAccess(adUnit, `ortb2Imp.ext.data.${MODULE_NAME}_rtd`, {}); - _each(Object.assign({}, dataUser, adUnitSegments), (v0, k) => { - const v = targetingNormalise(v0); - if (v) a[adUnit.code][ADSERVER_TARGETING_PREFIX + k] = v; + const ortb2imp = deepAccess(unit, `ortb2Imp.ext.data.${MODULE_NAME}_rtd`); + _each(ortb2imp, function(v0, k) { + const v = val(v0); + if (v) targeting[unit.code][`${ADSERVER_TARGETING_PREFIX}_${k}`] = v; }); - return a; - }, {}); + // ATF results shamelessly exfiltrated from intersectionRtdProvider + const bid = unit.bids.find(bid => !!bid.intersection); + if (bid) { + const v = val(config.params.thresholds.filter(t => t <= (bid.intersection.intersectionRatio * 100))); + if (v) targeting[unit.code][`${ADSERVER_TARGETING_PREFIX}_atf`] = v; + } + }); + + return targeting; } export const subModuleObj = { name: MODULE_NAME, init, getBidRequestData, - getTargetingData, - atf // used by adlooxRtdProvider_spec.js + getTargetingData }; submodule('realTimeData', subModuleObj); diff --git a/modules/adlooxRtdProvider.md b/modules/adlooxRtdProvider.md index 466f8ed1ba2..9d6a20a01a7 100644 --- a/modules/adlooxRtdProvider.md +++ b/modules/adlooxRtdProvider.md @@ -19,12 +19,7 @@ This provider fetches segments and populates the [First Party Data](https://docs * AdUnit segments are placed into `AdUnit.ortb2Imp.ext.data.adloox_rtd`: * **`{dis,vid,aud}`:** an list of integers describing the likelihood the AdUnit will be visible * **`atf`:** an list of integers describing the percentage of pixels visible at auction - * measured only once at pre-auction - * usable when the publisher uses the strategy of collapsing ad slots on no-fill - * using the reverse strategy, growing ad slots on fill, invalidates the measurement the position of all content (including the slots) changes post-auction - * works best when your page loads your ad slots have their actual size rendered (ie. not zero height) - * uses the smallest ad unit (above a threshold area of 20x20) supplied by the [publisher to Prebid.js](https://docs.prebid.org/dev-docs/examples/basic-example.html) and measures viewability as if that size to be used - * when used in cross-origin (unfriendly) IFRAME environments the ad slot is directly measured as is (ignoring publisher provided sizes) due to limitations in using [IntersectionObserver](https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserver) + * measured at pre-auction time using the [Intersection Module](https://docs.prebid.org/dev-docs/modules/intersectionRtdProvider.html); if not enabled then this measurement is not available **N.B.** this provider does not offer or utilise any user orientated data @@ -57,8 +52,13 @@ To use this, you *must* also integrate the [Adloox Analytics Adapter](./adlooxAn realTimeData: { auctionDelay: 100, // see below for guidance dataProviders: [ + { + name: 'intersection', + waitForIt: true + }, { name: 'adloox', + waitForIt: true, params: { // optional, defaults shown thresholds: [ 50, 60, 70, 80, 90 ], slotinpath: false diff --git a/test/spec/modules/adlooxRtdProvider_spec.js b/test/spec/modules/adlooxRtdProvider_spec.js index 236e053e58c..5b99789981f 100644 --- a/test/spec/modules/adlooxRtdProvider_spec.js +++ b/test/spec/modules/adlooxRtdProvider_spec.js @@ -1,5 +1,6 @@ import adapterManager from 'src/adapterManager.js'; import analyticsAdapter from 'modules/adlooxAnalyticsAdapter.js'; +import {auctionManager} from 'src/auctionManager.js'; import { config as _config } from 'src/config.js'; import { expect } from 'chai'; import * as events from 'src/events.js'; @@ -75,14 +76,6 @@ describe('Adloox RTD Provider', function () { done(); }); - it('should reject non-string config.params.api_origin', function (done) { - const ret = rtdProvider.init({ params: { api_origin: null } }); - - expect(ret).is.false; - - done(); - }); - it('should reject less than one config.params.imps', function (done) { const ret = rtdProvider.init({ params: { imps: 0 } }); @@ -147,31 +140,37 @@ describe('Adloox RTD Provider', function () { }); let server = null; - let __config = null, CONFIG = null; - let getConfigStub, setConfigStub; + let CONFIG = null; beforeEach(function () { server = sinon.createFakeServer(); - __config = {}; CONFIG = utils.deepClone(config); - getConfigStub = sinon.stub(_config, 'getConfig').callsFake(function (path) { - return utils.deepAccess(__config, path); - }); - setConfigStub = sinon.stub(_config, 'setConfig').callsFake(function (obj) { - utils.mergeDeep(__config, obj); - }); }); afterEach(function () { - setConfigStub.restore(); - getConfigStub.restore(); - getConfigStub = setConfigStub = undefined; CONFIG = null; - __config = null; server.restore(); server = null; }); it('should fetch segments', function (done) { - const req = {}; + const req = { + adUnitCodes: [ adUnit.code ], + ortb2Fragments: { + global: { + site: { + ext: { + data: { + } + } + }, + user: { + ext: { + data: { + } + } + } + } + } + }; const adUnitWithSegments = utils.deepClone(adUnit); const getGlobalStub = sinon.stub(prebidGlobal, 'getGlobal').returns({ adUnits: [ adUnitWithSegments ] @@ -201,119 +200,28 @@ describe('Adloox RTD Provider', function () { }); it('should set ad server targeting', function (done) { - utils.deepSetValue(__config, 'ortb2.site.ext.data.adloox_rtd.ok', true); - const adUnitWithSegments = utils.deepClone(adUnit); utils.deepSetValue(adUnitWithSegments, 'ortb2Imp.ext.data.adloox_rtd.dis', [ 50, 60 ]); const getGlobalStub = sinon.stub(prebidGlobal, 'getGlobal').returns({ adUnits: [ adUnitWithSegments ] }); - const targetingData = rtdProvider.getTargetingData([ adUnitWithSegments.code ], CONFIG, null, { - getFPD: () => ({ - global: __config.ortb2 - }) + const auction = { adUnits: [ adUnitWithSegments ] }; + const getAuctionStub = sinon.stub(auctionManager.index, 'getAuction').returns({ + adUnits: [ adUnitWithSegments ], + getFPD: () => { return { global: { site: { ext: { data: { adloox_rtd: { ok: true } } } } } } } }); + + const targetingData = rtdProvider.getTargetingData([ adUnitWithSegments.code ], CONFIG, null, auction); expect(Object.keys(targetingData).length).is.equal(1); expect(Object.keys(targetingData[adUnit.code]).length).is.equal(2); expect(targetingData[adUnit.code].adl_ok).is.equal(1); expect(targetingData[adUnit.code].adl_dis.length).is.equal(2); + getAuctionStub.restore(); getGlobalStub.restore(); done(); }); }); - - describe('measure atf', function () { - const adUnitCopy = utils.deepClone(adUnit); - - const ratio = 0.38; - const [ [width, height] ] = utils.getAdUnitSizes(adUnitCopy); - - before(function () { - adapterManager.enableAnalytics({ - provider: analyticsAdapterName, - options: analyticsOptions - }); - expect(analyticsAdapter.context).is.not.null; - }); - - after(function () { - analyticsAdapter.disableAnalytics(); - expect(analyticsAdapter.context).is.null; - }); - - it(`should return ${ratio} for same-origin`, function (done) { - const el = document.createElement('div'); - el.setAttribute('id', adUnitCopy.code); - - const offset = height * ratio; - const elStub = sinon.stub(el, 'getBoundingClientRect').returns({ - top: 0 - (height - offset), - bottom: height - offset, - left: 0, - right: width - }); - - const querySelectorStub = sinon.stub(document, 'querySelector'); - querySelectorStub.withArgs(`#${adUnitCopy.code}`).returns(el); - - rtdProvider.atf(adUnitCopy, function(x) { - expect(x).is.equal(ratio); - - querySelectorStub.restore(); - elStub.restore(); - - done(); - }); - }); - - ('IntersectionObserver' in window ? it : it.skip)(`should return ${ratio} for cross-origin`, function (done) { - const frameElementStub = sinon.stub(window, 'frameElement').value(null); - - const el = document.createElement('div'); - el.setAttribute('id', adUnitCopy.code); - - const elStub = sinon.stub(el, 'getBoundingClientRect').returns({ - top: 0, - bottom: height, - left: 0, - right: width - }); - - const querySelectorStub = sinon.stub(document, 'querySelector'); - querySelectorStub.withArgs(`#${adUnitCopy.code}`).returns(el); - - let intersectionObserverStubFn = null; - const intersectionObserverStub = sinon.stub(window, 'IntersectionObserver').callsFake((fn) => { - intersectionObserverStubFn = fn; - return { - observe: (element) => { - expect(element).is.equal(el); - - intersectionObserverStubFn([{ - target: element, - intersectionRect: { width, height }, - intersectionRatio: ratio - }]); - }, - unobserve: (element) => { - expect(element).is.equal(el); - } - } - }); - - rtdProvider.atf(adUnitCopy, function(x) { - expect(x).is.equal(ratio); - - intersectionObserverStub.restore(); - querySelectorStub.restore(); - elStub.restore(); - frameElementStub.restore(); - - done(); - }); - }); - }); });