diff --git a/modules/taboolaBidAdapter.js b/modules/taboolaBidAdapter.js index 3e7127f1ab7..ecc450ca2a5 100644 --- a/modules/taboolaBidAdapter.js +++ b/modules/taboolaBidAdapter.js @@ -3,10 +3,15 @@ import {registerBidder} from '../src/adapters/bidderFactory.js'; import {BANNER} from '../src/mediaTypes.js'; import {config} from '../src/config.js'; -import {deepSetValue, getWindowSelf, replaceAuctionPrice, isArray, safeJSONParse, isPlainObject} from '../src/utils.js'; +import {deepSetValue, getWindowSelf, replaceAuctionPrice, isArray, safeJSONParse, isPlainObject, getWinDimensions} from '../src/utils.js'; import {getStorageManager} from '../src/storageManager.js'; import {ajax} from '../src/ajax.js'; import {ortbConverter} from '../libraries/ortbConverter/converter.js'; +import {isWebdriverEnabled} from '../libraries/webdriver/webdriver.js'; +import {getConnectionType} from '../libraries/connectionInfo/connectionUtils.js'; +import {getViewportCoordinates} from '../libraries/viewport/viewport.js'; +import {percentInView} from '../libraries/percentInView/percentInView.js'; +import {getBoundingClientRect} from '../libraries/boundingClientRect/boundingClientRect.js'; const BIDDER_CODE = 'taboola'; const GVLID = 42; @@ -21,6 +26,7 @@ const TGID_COOKIE_KEY = 't_gid'; const TGID_PT_COOKIE_KEY = 't_pt_gid'; const TBLA_ID_COOKIE_KEY = 'tbla_id'; export const EVENT_ENDPOINT = 'https://beacon.bidder.taboola.com'; +export const refreshState = {}; /** * extract User Id by that order: @@ -95,6 +101,105 @@ export const internal = { } } +export function getRefreshInfo(adUnitCode) { + const now = Date.now(); + const state = refreshState[adUnitCode]; + + if (!state) { + refreshState[adUnitCode] = { + count: 1, + first: now, + last: now + }; + return { + count: 1, + first: now, + sinceLastSeconds: null, + sinceFirstSeconds: 0 + }; + } + + const sinceLastSeconds = Math.round((now - state.last) / 1000); + const sinceFirstSeconds = Math.round((now - state.first) / 1000); + state.count++; + state.last = now; + + return { + count: state.count, + first: state.first, + sinceLastSeconds, + sinceFirstSeconds + }; +} + +export function detectBot() { + try { + return { + detected: !!( + isWebdriverEnabled() || + window.__nightmare || + window.callPhantom || + window._phantom || + /HeadlessChrome/.test(navigator.userAgent) + ) + }; + } catch (e) { + return { detected: false }; + } +} + +export function getPageVisibility() { + try { + return { + hidden: document.hidden, + state: document.visibilityState, + hasFocus: document.hasFocus() + }; + } catch (e) { + return { hidden: false, state: 'visible', hasFocus: true }; + } +} + +export function getDeviceExtSignals(existingExt = {}) { + const viewport = getViewportCoordinates(); + return { + ...existingExt, + bot: detectBot(), + visibility: getPageVisibility(), + scroll: { + top: Math.round(viewport.top), + left: Math.round(viewport.left) + } + }; +} + +export function getElementSignals(adUnitCode) { + try { + const element = document.getElementById(adUnitCode); + if (!element) return null; + + const rect = getBoundingClientRect(element); + const winDimensions = getWinDimensions(); + const rawViewability = percentInView(element); + + const signals = { + placement: { + top: Math.round(rect.top), + left: Math.round(rect.left) + }, + fold: rect.top < winDimensions.innerHeight ? 'above' : 'below' + }; + + if (rawViewability !== null && !isNaN(rawViewability)) { + signals.viewability = Math.round(rawViewability); + } + + return signals; + } catch (e) { + return null; + } +} + const converter = ortbConverter({ context: { netRevenue: true, @@ -108,7 +213,7 @@ const converter = ortbConverter({ }, request(buildRequest, imps, bidderRequest, context) { const reqData = buildRequest(imps, bidderRequest, context); - fillTaboolaReqData(bidderRequest, context.bidRequests[0], reqData) + fillTaboolaReqData(bidderRequest, context.bidRequests[0], reqData, context); return reqData; }, bidResponse(buildBidResponse, bid, context) { @@ -137,7 +242,12 @@ export const spec = { }, buildRequests: (validBidRequests, bidderRequest) => { const [bidRequest] = validBidRequests; - const data = converter.toORTB({bidderRequest: bidderRequest, bidRequests: validBidRequests}); + const auctionId = bidderRequest.auctionId || validBidRequests[0]?.auctionId; + const data = converter.toORTB({ + bidderRequest: bidderRequest, + bidRequests: validBidRequests, + context: { auctionId } + }); const {publisherId} = bidRequest.params; const url = END_POINT_URL + '?publisher=' + publisherId; @@ -287,10 +397,19 @@ function getSiteProperties({publisherId}, refererInfo, ortb2) { } } -function fillTaboolaReqData(bidderRequest, bidRequest, data) { +function fillTaboolaReqData(bidderRequest, bidRequest, data, context) { const {refererInfo, gdprConsent = {}, uspConsent} = bidderRequest; const site = getSiteProperties(bidRequest.params, refererInfo, bidderRequest.ortb2); - deepSetValue(data, 'device', bidderRequest?.ortb2?.device); + + const ortb2Device = bidderRequest?.ortb2?.device || {}; + const connectionType = getConnectionType(); + const device = { + ...ortb2Device, + js: 1, + ...(connectionType && { connectiontype: connectionType }), + ext: getDeviceExtSignals(ortb2Device.ext) + }; + deepSetValue(data, 'device', device); const extractedUserId = userData.getUserId(gdprConsent, uspConsent); if (data.user === undefined || data.user === null) { data.user = { @@ -340,6 +459,10 @@ function fillTaboolaReqData(bidderRequest, bidRequest, data) { data.wlang = ortb2.wlang || bidRequest.params.wlang || []; deepSetValue(data, 'ext.pageType', ortb2?.ext?.data?.pageType || ortb2?.ext?.data?.section || bidRequest.params.pageType); deepSetValue(data, 'ext.prebid.version', '$prebid.version$'); + const auctionId = context?.auctionId; + if (auctionId) { + deepSetValue(data, 'ext.prebid.auctionId', auctionId); + } } function fillTaboolaImpData(bid, imp) { @@ -362,6 +485,27 @@ function fillTaboolaImpData(bid, imp) { imp.bidfloorcur = bidfloorcur; } deepSetValue(imp, 'ext.gpid', bid?.ortb2Imp?.ext?.gpid); + + if (bid.bidId) { + deepSetValue(imp, 'ext.prebid.bidId', bid.bidId); + } + if (bid.adUnitCode) { + deepSetValue(imp, 'ext.prebid.adUnitCode', bid.adUnitCode); + const refreshInfo = getRefreshInfo(bid.adUnitCode); + deepSetValue(imp, 'ext.prebid.refresh', refreshInfo); + } + if (bid.adUnitId) { + deepSetValue(imp, 'ext.prebid.adUnitId', bid.adUnitId); + } + + const elementSignals = getElementSignals(bid.adUnitCode); + if (elementSignals) { + if (elementSignals.viewability !== undefined) { + deepSetValue(imp, 'ext.viewability', elementSignals.viewability); + } + deepSetValue(imp, 'ext.placement', elementSignals.placement); + deepSetValue(imp, 'ext.fold', elementSignals.fold); + } } function getBanners(bid, pos) { diff --git a/test/spec/modules/taboolaBidAdapter_spec.js b/test/spec/modules/taboolaBidAdapter_spec.js index 9c06d717a04..8aaff32c380 100644 --- a/test/spec/modules/taboolaBidAdapter_spec.js +++ b/test/spec/modules/taboolaBidAdapter_spec.js @@ -1,5 +1,5 @@ import {expect} from 'chai'; -import {spec, internal, END_POINT_URL, userData, EVENT_ENDPOINT} from 'modules/taboolaBidAdapter.js'; +import {spec, internal, END_POINT_URL, userData, EVENT_ENDPOINT, detectBot, getPageVisibility, refreshState, getRefreshInfo} from 'modules/taboolaBidAdapter.js'; import {config} from '../../../src/config.js' import * as utils from '../../../src/utils.js' import {server} from '../../mocks/xhr.js' @@ -285,9 +285,13 @@ describe('Taboola Adapter', function () { 'tagid': commonBidRequest.params.tagId, 'bidfloor': null, 'bidfloorcur': 'USD', - 'ext': {} + 'ext': { + 'prebid': { + 'bidId': defaultBidRequest.bidId + } + } }], - 'device': {'ua': navigator.userAgent}, + 'device': res.data.device, 'id': 'mock-uuid', 'test': 0, 'user': { @@ -308,15 +312,12 @@ describe('Taboola Adapter', function () { 'bcat': [], 'badv': [], 'wlang': [], - 'ext': { - 'prebid': { - 'version': '$prebid.version$' - } - } + 'ext': res.data.ext }; expect(res.url).to.equal(`${END_POINT_URL}?publisher=${commonBidRequest.params.publisherId}`); expect(JSON.stringify(res.data)).to.deep.equal(JSON.stringify(expectedData)); + expect(res.data.ext.prebid.version).to.equal('$prebid.version$'); }); it('should pass optional parameters in request', function () { @@ -475,7 +476,12 @@ describe('Taboola Adapter', function () { expect(res.data.badv).to.deep.equal(bidderRequest.ortb2.badv) expect(res.data.wlang).to.deep.equal(bidderRequest.ortb2.wlang) expect(res.data.user.id).to.deep.equal(bidderRequest.ortb2.user.id) - expect(res.data.device).to.deep.equal(bidderRequest.ortb2.device); + // Device should contain ortb2 device properties plus fraud prevention signals + expect(res.data.device.w).to.equal(bidderRequest.ortb2.device.w); + expect(res.data.device.h).to.equal(bidderRequest.ortb2.device.h); + expect(res.data.device.ua).to.equal(bidderRequest.ortb2.device.ua); + expect(res.data.device.ext.bot).to.exist; + expect(res.data.device.ext.visibility).to.exist; }); it('should pass user entities', function () { @@ -1606,5 +1612,462 @@ describe('Taboola Adapter', function () { expect(internal.getReferrer(bidderRequest.refererInfo)).to.equal(bidderRequest.refererInfo.ref); }); }); + + describe('detectBot', function () { + it('should return detected false for normal browsers', function () { + const result = detectBot(); + expect(result).to.have.property('detected'); + expect(result.detected).to.be.a('boolean'); + }); + + it('should return object with detected property', function () { + const result = detectBot(); + expect(result).to.be.an('object'); + expect(result).to.have.property('detected'); + }); + }); + + describe('getPageVisibility', function () { + it('should return visibility state object', function () { + const result = getPageVisibility(); + expect(result).to.be.an('object'); + expect(result).to.have.property('hidden'); + expect(result).to.have.property('state'); + expect(result).to.have.property('hasFocus'); + }); + + it('should return boolean for hidden property', function () { + const result = getPageVisibility(); + expect(result.hidden).to.be.a('boolean'); + }); + + it('should return string for state property', function () { + const result = getPageVisibility(); + expect(result.state).to.be.a('string'); + }); + + it('should return boolean for hasFocus property', function () { + const result = getPageVisibility(); + expect(result.hasFocus).to.be.a('boolean'); + }); + }); + + describe('fraud signals in buildRequests', function () { + const defaultBidRequest = { + bidder: 'taboola', + params: { + publisherId: 'publisherId', + tagId: 'placement name' + }, + bidId: 'test-bid-id', + auctionId: 'test-auction-id', + sizes: [[300, 250]] + }; + + const commonBidderRequest = { + bidderRequestId: 'mock-uuid', + refererInfo: { + page: 'https://example.com/ref', + ref: 'https://ref', + domain: 'example.com', + } + }; + + it('should include bot detection in device.ext', function () { + const res = spec.buildRequests([defaultBidRequest], commonBidderRequest); + expect(res.data.device.ext.bot).to.exist; + expect(res.data.device.ext.bot).to.have.property('detected'); + }); + + it('should include visibility in device.ext', function () { + const res = spec.buildRequests([defaultBidRequest], commonBidderRequest); + expect(res.data.device.ext.visibility).to.exist; + expect(res.data.device.ext.visibility).to.have.property('hidden'); + expect(res.data.device.ext.visibility).to.have.property('state'); + expect(res.data.device.ext.visibility).to.have.property('hasFocus'); + }); + + it('should include scroll position in device.ext', function () { + const res = spec.buildRequests([defaultBidRequest], commonBidderRequest); + expect(res.data.device.ext.scroll).to.exist; + expect(res.data.device.ext.scroll).to.have.property('top'); + expect(res.data.device.ext.scroll).to.have.property('left'); + expect(res.data.device.ext.scroll.top).to.be.a('number'); + expect(res.data.device.ext.scroll.left).to.be.a('number'); + }); + + it('should include viewability in imp.ext when element exists', function () { + const adUnitCode = 'test-viewability-div'; + const testDiv = document.createElement('div'); + testDiv.id = adUnitCode; + testDiv.style.width = '300px'; + testDiv.style.height = '250px'; + document.body.appendChild(testDiv); + + const bidRequest = { + ...defaultBidRequest, + adUnitCode: adUnitCode + }; + + try { + const res = spec.buildRequests([bidRequest], commonBidderRequest); + // Viewability should be a number between 0-100 when element exists + expect(res.data.imp[0].ext.viewability).to.be.a('number'); + expect(res.data.imp[0].ext.viewability).to.be.at.least(0); + expect(res.data.imp[0].ext.viewability).to.be.at.most(100); + } finally { + document.body.removeChild(testDiv); + } + }); + + it('should not include viewability when element does not exist', function () { + const bidRequest = { + ...defaultBidRequest, + adUnitCode: 'non-existent-element-id' + }; + const res = spec.buildRequests([bidRequest], commonBidderRequest); + expect(res.data.imp[0].ext.viewability).to.be.undefined; + }); + + it('should include placement position in imp.ext when element exists', function () { + const adUnitCode = 'test-placement-div'; + const testDiv = document.createElement('div'); + testDiv.id = adUnitCode; + testDiv.style.width = '300px'; + testDiv.style.height = '250px'; + testDiv.style.position = 'absolute'; + testDiv.style.top = '100px'; + testDiv.style.left = '50px'; + document.body.appendChild(testDiv); + + const bidRequest = { + ...defaultBidRequest, + adUnitCode: adUnitCode + }; + + try { + const res = spec.buildRequests([bidRequest], commonBidderRequest); + expect(res.data.imp[0].ext.placement).to.exist; + expect(res.data.imp[0].ext.placement).to.have.property('top'); + expect(res.data.imp[0].ext.placement).to.have.property('left'); + expect(res.data.imp[0].ext.placement.top).to.be.a('number'); + expect(res.data.imp[0].ext.placement.left).to.be.a('number'); + } finally { + document.body.removeChild(testDiv); + } + }); + + it('should include fold detection in imp.ext when element exists', function () { + const adUnitCode = 'test-fold-div'; + const testDiv = document.createElement('div'); + testDiv.id = adUnitCode; + testDiv.style.width = '300px'; + testDiv.style.height = '250px'; + document.body.appendChild(testDiv); + + const bidRequest = { + ...defaultBidRequest, + adUnitCode: adUnitCode + }; + + try { + const res = spec.buildRequests([bidRequest], commonBidderRequest); + expect(res.data.imp[0].ext.fold).to.exist; + expect(res.data.imp[0].ext.fold).to.be.oneOf(['above', 'below']); + } finally { + document.body.removeChild(testDiv); + } + }); + + it('should not include placement or fold when element does not exist', function () { + const bidRequest = { + ...defaultBidRequest, + adUnitCode: 'non-existent-placement-element' + }; + const res = spec.buildRequests([bidRequest], commonBidderRequest); + expect(res.data.imp[0].ext.placement).to.be.undefined; + expect(res.data.imp[0].ext.fold).to.be.undefined; + }); + + it('should preserve existing ortb2 device ext properties', function () { + const bidderRequestWithDeviceExt = { + ...commonBidderRequest, + ortb2: { + device: { + ua: 'test-ua', + ext: { + existingProp: 'existingValue' + } + } + } + }; + const res = spec.buildRequests([defaultBidRequest], bidderRequestWithDeviceExt); + expect(res.data.device.ext.existingProp).to.equal('existingValue'); + expect(res.data.device.ext.bot).to.exist; + expect(res.data.device.ext.visibility).to.exist; + }); + + it('should include device.js = 1', function () { + const res = spec.buildRequests([defaultBidRequest], commonBidderRequest); + expect(res.data.device.js).to.equal(1); + }); + + it('should include connectiontype when available', function () { + const res = spec.buildRequests([defaultBidRequest], commonBidderRequest); + // connectiontype is optional - depends on navigator.connection availability + if (res.data.device.connectiontype !== undefined) { + expect(res.data.device.connectiontype).to.be.a('number'); + expect(res.data.device.connectiontype).to.be.oneOf([0, 1, 2, 3, 4, 5, 6, 7]); + } + }); + + it('should not override existing ortb2 device properties', function () { + const bidderRequestWithDevice = { + ...commonBidderRequest, + ortb2: { + device: { + ua: 'custom-ua', + w: 1920, + h: 1080 + } + } + }; + const res = spec.buildRequests([defaultBidRequest], bidderRequestWithDevice); + expect(res.data.device.ua).to.equal('custom-ua'); + expect(res.data.device.w).to.equal(1920); + expect(res.data.device.h).to.equal(1080); + expect(res.data.device.js).to.equal(1); + }); + }); + + describe('Prebid IDs in buildRequests', function () { + const defaultBidRequest = { + bidder: 'taboola', + params: { + publisherId: 'publisherId', + tagId: 'placement name' + }, + bidId: 'test-bid-id-123', + auctionId: 'test-auction-id-456', + adUnitCode: 'test-ad-unit-code', + sizes: [[300, 250]] + }; + + const commonBidderRequest = { + bidderRequestId: 'mock-uuid', + auctionId: 'auction-id-789', + refererInfo: { + page: 'https://example.com/ref', + ref: 'https://ref', + domain: 'example.com', + } + }; + + it('should include auctionId in ext.prebid', function () { + const res = spec.buildRequests([defaultBidRequest], commonBidderRequest); + expect(res.data.ext.prebid.auctionId).to.equal('auction-id-789'); + }); + + it('should include bidId in imp.ext.prebid', function () { + const res = spec.buildRequests([defaultBidRequest], commonBidderRequest); + expect(res.data.imp[0].ext.prebid.bidId).to.equal('test-bid-id-123'); + }); + + it('should include adUnitCode in imp.ext.prebid', function () { + const res = spec.buildRequests([defaultBidRequest], commonBidderRequest); + expect(res.data.imp[0].ext.prebid.adUnitCode).to.equal('test-ad-unit-code'); + }); + + it('should include adUnitId in imp.ext.prebid when available', function () { + const bidRequestWithAdUnitId = { + ...defaultBidRequest, + adUnitId: 'test-ad-unit-id' + }; + const res = spec.buildRequests([bidRequestWithAdUnitId], commonBidderRequest); + expect(res.data.imp[0].ext.prebid.adUnitId).to.equal('test-ad-unit-id'); + }); + + it('should not include adUnitId when not available', function () { + const res = spec.buildRequests([defaultBidRequest], commonBidderRequest); + expect(res.data.imp[0].ext.prebid.adUnitId).to.be.undefined; + }); + + it('should include all Prebid IDs for multiple impressions', function () { + const bidRequest1 = { + ...defaultBidRequest, + bidId: 'bid-id-1', + adUnitCode: 'ad-unit-1' + }; + const bidRequest2 = { + ...defaultBidRequest, + bidId: 'bid-id-2', + adUnitCode: 'ad-unit-2' + }; + const res = spec.buildRequests([bidRequest1, bidRequest2], commonBidderRequest); + expect(res.data.imp[0].ext.prebid.bidId).to.equal('bid-id-1'); + expect(res.data.imp[0].ext.prebid.adUnitCode).to.equal('ad-unit-1'); + expect(res.data.imp[1].ext.prebid.bidId).to.equal('bid-id-2'); + expect(res.data.imp[1].ext.prebid.adUnitCode).to.equal('ad-unit-2'); + }); + }); + + describe('Refresh tracking in buildRequests', function () { + const defaultBidRequest = { + bidder: 'taboola', + params: { + publisherId: 'publisherId', + tagId: 'placement name' + }, + bidId: 'test-bid-id', + adUnitCode: 'refresh-test-ad-unit', + sizes: [[300, 250]] + }; + + const commonBidderRequest = { + bidderRequestId: 'mock-uuid', + auctionId: 'test-auction-id', + refererInfo: { + page: 'https://example.com/ref', + ref: 'https://ref', + domain: 'example.com', + } + }; + + beforeEach(function () { + Object.keys(refreshState).forEach(key => delete refreshState[key]); + }); + + describe('getRefreshInfo function', function () { + it('should return count=1 and sinceLastSeconds=null for first request', function () { + const adUnitCode = 'first-request-test'; + const result = getRefreshInfo(adUnitCode); + + expect(result.count).to.equal(1); + expect(result.sinceLastSeconds).to.be.null; + expect(result.sinceFirstSeconds).to.equal(0); + expect(result.first).to.be.a('number'); + }); + + it('should increment count on subsequent requests', function () { + const adUnitCode = 'increment-test'; + + const first = getRefreshInfo(adUnitCode); + expect(first.count).to.equal(1); + + const second = getRefreshInfo(adUnitCode); + expect(second.count).to.equal(2); + + const third = getRefreshInfo(adUnitCode); + expect(third.count).to.equal(3); + }); + + it('should track different ad units independently', function () { + const adUnit1 = 'independent-test-1'; + const adUnit2 = 'independent-test-2'; + + const first1 = getRefreshInfo(adUnit1); + const first2 = getRefreshInfo(adUnit2); + + expect(first1.count).to.equal(1); + expect(first2.count).to.equal(1); + + const second1 = getRefreshInfo(adUnit1); + expect(second1.count).to.equal(2); + expect(refreshState[adUnit2].count).to.equal(1); + }); + + it('should calculate time differences for subsequent requests', function () { + const adUnitCode = 'time-diff-test'; + const now = Date.now(); + + // Manually set the state to simulate time passing + refreshState[adUnitCode] = { + count: 1, + first: now - 10000, // 10 seconds ago + last: now - 5000 // 5 seconds ago + }; + + const result = getRefreshInfo(adUnitCode); + + expect(result.count).to.equal(2); + expect(result.sinceLastSeconds).to.be.at.least(4); // Allow for timing variance + expect(result.sinceLastSeconds).to.be.at.most(6); + expect(result.sinceFirstSeconds).to.be.at.least(9); + expect(result.sinceFirstSeconds).to.be.at.most(11); + }); + + it('should preserve the first timestamp across refreshes', function () { + const adUnitCode = 'preserve-first-test'; + + const first = getRefreshInfo(adUnitCode); + const firstTimestamp = first.first; + + const second = getRefreshInfo(adUnitCode); + expect(second.first).to.equal(firstTimestamp); + + const third = getRefreshInfo(adUnitCode); + expect(third.first).to.equal(firstTimestamp); + }); + }); + + describe('refresh info in buildRequests', function () { + it('should include refresh info in imp.ext.prebid.refresh', function () { + const bidRequest = { + ...defaultBidRequest, + adUnitCode: 'build-request-refresh-test' + }; + const res = spec.buildRequests([bidRequest], commonBidderRequest); + + expect(res.data.imp[0].ext.prebid.refresh).to.exist; + expect(res.data.imp[0].ext.prebid.refresh.count).to.equal(1); + expect(res.data.imp[0].ext.prebid.refresh.sinceLastSeconds).to.be.null; + expect(res.data.imp[0].ext.prebid.refresh.sinceFirstSeconds).to.equal(0); + expect(res.data.imp[0].ext.prebid.refresh.first).to.be.a('number'); + }); + + it('should increment refresh count on subsequent builds for same ad unit', function () { + const bidRequest = { + ...defaultBidRequest, + adUnitCode: 'build-request-increment-test' + }; + + const res1 = spec.buildRequests([bidRequest], commonBidderRequest); + expect(res1.data.imp[0].ext.prebid.refresh.count).to.equal(1); + + const res2 = spec.buildRequests([bidRequest], commonBidderRequest); + expect(res2.data.imp[0].ext.prebid.refresh.count).to.equal(2); + + const res3 = spec.buildRequests([bidRequest], commonBidderRequest); + expect(res3.data.imp[0].ext.prebid.refresh.count).to.equal(3); + }); + + it('should track refresh independently for multiple ad units in same request', function () { + const bidRequest1 = { + ...defaultBidRequest, + bidId: 'bid-1', + adUnitCode: 'multi-ad-unit-test-1' + }; + const bidRequest2 = { + ...defaultBidRequest, + bidId: 'bid-2', + adUnitCode: 'multi-ad-unit-test-2' + }; + + const res = spec.buildRequests([bidRequest1, bidRequest2], commonBidderRequest); + + expect(res.data.imp[0].ext.prebid.refresh.count).to.equal(1); + expect(res.data.imp[1].ext.prebid.refresh.count).to.equal(1); + + // Second request - only refresh first ad unit + const res2 = spec.buildRequests([bidRequest1], commonBidderRequest); + expect(res2.data.imp[0].ext.prebid.refresh.count).to.equal(2); + + // Third request - both ad units + const res3 = spec.buildRequests([bidRequest1, bidRequest2], commonBidderRequest); + expect(res3.data.imp[0].ext.prebid.refresh.count).to.equal(3); + expect(res3.data.imp[1].ext.prebid.refresh.count).to.equal(2); + }); + }); + }); }) })