diff --git a/integrationExamples/gpt/responsiveAds_sizeMappingV2.html b/integrationExamples/gpt/responsiveAds_sizeMappingV2.html new file mode 100644 index 00000000000..d262af5199a --- /dev/null +++ b/integrationExamples/gpt/responsiveAds_sizeMappingV2.html @@ -0,0 +1,121 @@ + + + + + + + + + + + + + + +

Prebid.js Test

+
Div-1
+
+ +
+ + + diff --git a/modules/adpod.js b/modules/adpod.js index 46f7f80d47e..a84f4f16285 100644 --- a/modules/adpod.js +++ b/modules/adpod.js @@ -271,7 +271,7 @@ export function checkAdUnitSetupHook(fn, adUnits) { let errMsg = `Detected missing or incorrectly setup fields for an adpod adUnit. Please review the following fields of adUnitCode: ${adUnit.code}. This adUnit will be removed from the auction.`; - let playerSize = !!(videoConfig.playerSize && utils.isArrayOfNums(videoConfig.playerSize)); + let playerSize = !!((videoConfig.playerSize && utils.isArrayOfNums(videoConfig.playerSize)) || (videoConfig.sizeConfig)); let adPodDurationSec = !!(videoConfig.adPodDurationSec && utils.isNumber(videoConfig.adPodDurationSec) && videoConfig.adPodDurationSec > 0); let durationRangeSec = !!(videoConfig.durationRangeSec && utils.isArrayOfNums(videoConfig.durationRangeSec) && videoConfig.durationRangeSec.every(range => range > 0)); diff --git a/modules/sizeMappingV2.js b/modules/sizeMappingV2.js new file mode 100644 index 00000000000..ec3604faa97 --- /dev/null +++ b/modules/sizeMappingV2.js @@ -0,0 +1,541 @@ +/** + * This modules adds support for the new Size Mapping spec described here. https://github.com/prebid/Prebid.js/issues/4129 + * This implementation replaces global sizeConfig with a adUnit/bidder level sizeConfig with support for labels. + */ + +import * as utils from '../src/utils.js'; +import { processNativeAdUnitParams } from '../src/native.js'; +import { adunitCounter } from '../src/adUnits.js'; +import includes from 'core-js/library/fn/array/includes.js'; +import { getHook } from '../src/hook.js'; +import { + adUnitSetupChecks +} from '../src/prebid.js'; + +// allows for sinon.spy, sinon.stub, etc to unit test calls made to these functions internally +export const internal = { + checkBidderSizeConfigFormat, + getActiveSizeBucket, + getFilteredMediaTypes, + getAdUnitDetail, + getRelevantMediaTypesForBidder, + isLabelActivated +}; + +// 'sizeMappingInternalStore' contains information whether a particular auction is using size mapping V2 (the new size mapping spec), +// and it also contains additional information on each adUnit, as such, mediaTypes, activeViewport, etc. +// This information is required by the 'getBids' function. +export const sizeMappingInternalStore = createSizeMappingInternalStore(); + +function createSizeMappingInternalStore() { + const sizeMappingInternalStore = {}; + + return { + initializeStore: function(auctionId, isUsingSizeMappingBool) { + sizeMappingInternalStore[auctionId] = { + usingSizeMappingV2: isUsingSizeMappingBool, + adUnits: [] + }; + }, + getAuctionDetail: function(auctionId) { + return sizeMappingInternalStore[auctionId]; + }, + setAuctionDetail: function(auctionId, adUnitDetail) { + sizeMappingInternalStore[auctionId].adUnits.push(adUnitDetail); + } + } +} + +// returns "true" if atleast one of the adUnit in the adUnits array has declared a Ad Unit or(and) Bidder level sizeConfig +// returns "false" otherwise +export function isUsingNewSizeMapping(adUnits) { + let isUsingSizeMappingBool = false; + adUnits.forEach(adUnit => { + if (adUnit.mediaTypes) { + // checks for the presence of sizeConfig property at the adUnit.mediaTypes object + Object.keys(adUnit.mediaTypes).forEach(mediaType => { + if (adUnit.mediaTypes[mediaType].sizeConfig) { + if (isUsingSizeMappingBool === false) { + isUsingSizeMappingBool = true; + } + } + }); + + // checks for the presence of sizeConfig property at the adUnit.bids[].bidder object + adUnit.bids.forEach(bidder => { + if (bidder.sizeConfig) { + if (isUsingSizeMappingBool === false) { + isUsingSizeMappingBool = true; + } + } + }); + } + }); + return isUsingSizeMappingBool; +} + +// returns "adUnits" array which have passed sizeConfig validation checks in addition to mediaTypes checks +// deletes properties from adUnit which fail validation. +export function checkAdUnitSetupHook(adUnits) { + return adUnits.filter(adUnit => { + const mediaTypes = adUnit.mediaTypes; + if (!mediaTypes || Object.keys(mediaTypes).length === 0) { + utils.logError(`Detected adUnit.code '${adUnit.code}' did not have a 'mediaTypes' object defined. This is a required field for the auction, so this adUnit has been removed.`); + return false; + } + + if (mediaTypes.banner) { + const banner = mediaTypes.banner; + if (banner.sizes) { + adUnitSetupChecks.validateBannerMediaType(adUnit); + } else if (banner.sizeConfig) { + if (Array.isArray(banner.sizeConfig)) { + let deleteBannerMediaType = false; + banner.sizeConfig.forEach((config, index) => { + // verify if all config objects include "minViewPort" and "sizes" property. + // if not, remove the mediaTypes.banner object + const keys = Object.keys(config); + if (!(includes(keys, 'minViewPort') && includes(keys, 'sizes'))) { + utils.logError(`Ad Unit: ${adUnit.code}: mediaTypes.banner.sizeConfig[${index}] is missing required property minViewPort or sizes or both.`); + deleteBannerMediaType = true; + return; + } + + // check if the config.sizes property is in [w, h] format, if yes, change it to [[w, h]] format. + const bannerSizes = adUnitSetupChecks.validateSizes(config.sizes); + if (utils.isArrayOfNums(config.minViewPort, 2)) { + if (config.sizes.length > 0 && bannerSizes.length > 0) { + config.sizes = bannerSizes; + } else if (config.sizes.length === 0) { + // If a size bucket doesn't have any sizes, sizes is an empty array, i.e. sizes: []. This check takes care of that. + config.sizes = [config.sizes]; + } else { + utils.logError(`Ad Unit: ${adUnit.code}: mediaTypes.banner.sizeConfig[${index}] has propery sizes declared with invalid value. Please ensure the sizes are listed like: [[300, 250], ...] or like: [] if no sizes are present for that size bucket.`); + deleteBannerMediaType = true; + } + } else { + utils.logError(`Ad Unit: ${adUnit.code}: mediaTypes.banner.sizeConfig[${index}] has property minViewPort decalared with invalid value. Please ensure minViewPort is an Array and is listed like: [700, 0]. Declaring an empty array is not allowed, instead use: [0, 0].`); + deleteBannerMediaType = true; + } + }); + if (deleteBannerMediaType) { + utils.logInfo(`Ad Unit: ${adUnit.code}: mediaTypes.banner has been removed due to error in sizeConfig.`); + delete adUnit.mediaTypes.banner; + } + } else { + utils.logError(`Ad Unit: ${adUnit.code}: mediaTypes.banner.sizeConfig is NOT an Array. Removing the invalid object mediaTypes.banner from Ad Unit.`); + delete adUnit.mediaTypes.banner; + } + } else { + utils.logError('Detected a mediaTypes.banner object did not include required property sizes or sizeConfig. Removing invalid mediaTypes.banner object from Ad Unit.'); + delete adUnit.mediaTypes.banner; + } + } + + if (mediaTypes.video) { + const video = mediaTypes.video; + if (video.playerSize) { + adUnitSetupChecks.validateVideoMediaType(adUnit); + } else if (video.sizeConfig) { + if (Array.isArray(video.sizeConfig)) { + let deleteVideoMediaType = false; + video.sizeConfig.forEach((config, index) => { + // verify if all config objects include "minViewPort" and "playerSize" property. + // if not, remove the mediaTypes.video object + const keys = Object.keys(config); + if (!(includes(keys, 'minViewPort') && includes(keys, 'playerSize'))) { + utils.logError(`Ad Unit: ${adUnit.code}: mediaTypes.video.sizeConfig[${index}] is missing required property minViewPort or playerSize or both. Removing the invalid property mediaTypes.video.sizeConfig from Ad Unit.`); + deleteVideoMediaType = true; + return; + } + // check if the config.playerSize property is in [w, h] format, if yes, change it to [[w, h]] format. + let tarPlayerSizeLen = (typeof config.playerSize[0] === 'number') ? 2 : 1; + const videoSizes = adUnitSetupChecks.validateSizes(config.playerSize, tarPlayerSizeLen); + if (utils.isArrayOfNums(config.minViewPort, 2)) { + if (tarPlayerSizeLen === 2) { + utils.logInfo('Transforming video.playerSize from [640,480] to [[640,480]] so it\'s in the proper format.'); + } + if (config.playerSize.length > 0 && videoSizes.length > 0) { + config.playerSize = videoSizes; + } else if (config.playerSize.length === 0) { + // If a size bucket doesn't have any playerSize, playerSize is an empty array, i.e. playerSize: []. This check takes care of that. + config.playerSize = [config.playerSize]; + } else { + utils.logError(`Ad Unit: ${adUnit.code}: mediaTypes.video.sizeConfig[${index}] has propery playerSize declared with invalid value. Please ensure the playerSize is listed like: [640, 480] or like: [] if no playerSize is present for that size bucket.`); + deleteVideoMediaType = true; + } + } else { + utils.logError(`Ad Unit: ${adUnit.code}: mediaTypes.video.sizeConfig[${index}] has property minViewPort decalared with invalid value. Please ensure minViewPort is an Array and is listed like: [700, 0]. Declaring an empty array is not allowed, instead use: [0, 0].`); + deleteVideoMediaType = true; + } + }); + if (deleteVideoMediaType) { + utils.logInfo(`Ad Unit: ${adUnit.code}: mediaTypes.video.sizeConfig has been removed due to error in sizeConfig.`); + delete adUnit.mediaTypes.video.sizeConfig; + } + } else { + utils.logError(`Ad Unit: ${adUnit.code}: mediaTypes.video.sizeConfig is NOT an Array. Removing the invalid property mediaTypes.video.sizeConfig from Ad Unit.`); + return delete adUnit.mediaTypes.video.sizeConfig; + } + } + } + + if (mediaTypes.native) { + const native = mediaTypes.native; + adUnitSetupChecks.validateNativeMediaType(adUnit); + + if (mediaTypes.native.sizeConfig) { + native.sizeConfig.forEach(config => { + // verify if all config objects include "minViewPort" and "active" property. + // if not, remove the mediaTypes.native object + const keys = Object.keys(config); + if (!(includes(keys, 'minViewPort') && includes(keys, 'active'))) { + utils.logError(`Ad Unit: ${adUnit.code}: mediaTypes.native.sizeConfig is missing required property minViewPort or active or both. Removing the invalid property mediaTypes.native.sizeConfig from Ad Unit.`); + return delete adUnit.mediaTypes.native.sizeConfig; + } + + if (!(utils.isArrayOfNums(config.minViewPort, 2) && typeof config.active === 'boolean')) { + utils.logError(`Ad Unit: ${adUnit.code}: mediaTypes.native.sizeConfig has properties minViewPort or active decalared with invalid values. Removing the invalid property mediaTypes.native.sizeConfig from Ad Unit.`); + return delete adUnit.mediaTypes.native.sizeConfig; + } + }); + } + } + + return true; + }); +} + +getHook('checkAdUnitSetup').before(function (fn, adUnits) { + const usingNewSizeMapping = isUsingNewSizeMapping(adUnits); + if (usingNewSizeMapping) { + // if adUnits are found using the sizeMappingV2 spec, we run additional checks on them for checking the validity of sizeConfig object + // in addition to running the base checks on the mediaType object and return the adUnit without calling the base function. + adUnits = checkAdUnitSetupHook(adUnits); + return fn.bail(adUnits); + } else { + // if presence of sizeMappingV2 spec is not detected on adUnits, we default back to the original checks defined in the base function. + return fn.call(this, adUnits); + } +}); + +// checks if the sizeConfig object declared at the Bidder level is in the right format or not. +export function checkBidderSizeConfigFormat(sizeConfig) { + let didCheckPass = true; + if (Array.isArray(sizeConfig) && sizeConfig.length > 0) { + sizeConfig.forEach(config => { + const keys = Object.keys(config); + if ((includes(keys, 'minViewPort') && + includes(keys, 'relevantMediaTypes')) && + utils.isArrayOfNums(config.minViewPort, 2) && + Array.isArray(config.relevantMediaTypes) && + config.relevantMediaTypes.length > 0 && + (config.relevantMediaTypes.length > 1 ? (config.relevantMediaTypes.every(mt => (includes(['banner', 'video', 'native'], mt)))) + : (['none', 'banner', 'video', 'native'].indexOf(config.relevantMediaTypes[0] > -1)))) { + didCheckPass = didCheckPass && true; + } else { + didCheckPass = false; + } + }); + } else { + didCheckPass = false; + } + return didCheckPass; +} + +getHook('getBids').before(function (fn, bidderInfo) { + // check if the adUnit is using sizeMappingV2 specs and store the result in _sizeMappingUsageMap. + if (typeof sizeMappingInternalStore.getAuctionDetail(bidderInfo.auctionId) === 'undefined') { + const isUsingSizeMappingBool = isUsingNewSizeMapping(bidderInfo.adUnits); + + // initialize sizeMappingInternalStore for the first time for a particular auction + sizeMappingInternalStore.initializeStore(bidderInfo.auctionId, isUsingSizeMappingBool); + } + if (sizeMappingInternalStore.getAuctionDetail(bidderInfo.auctionId).usingSizeMappingV2) { + // if adUnit is found using sizeMappingV2 specs, run the getBids function which processes the sizeConfig object + // and returns the bids array for a particular bidder. + const bids = getBids(bidderInfo); + return fn.bail(bids); + } else { + // if not using sizeMappingV2, default back to the getBids function defined in adapterManager. + return fn.call(this, bidderInfo); + } +}); + +/** + * Given an Ad Unit or a Bid as an input, returns a boolean telling if the Ad Unit/ Bid is active based on label checks + * @param {Object} bidOrAdUnit Either the Ad Unit object or the Bid object + * @param {Array} activeLabels List of active labels passed as an argument to pbjs.requestBids function + * @param {string} adUnitCode Unique string identifier for an Ad Unit. + * @returns {boolean} Represents if the Ad Unit or the Bid is active or not + */ +export function isLabelActivated(bidOrAdUnit, activeLabels, adUnitCode) { + let labelOperator; + const labelsFound = Object.keys(bidOrAdUnit).filter(prop => prop === 'labelAny' || prop === 'labelAll'); + if (labelsFound && labelsFound.length > 1) { + utils.logWarn(`SizeMappingV2:: ${(bidOrAdUnit.code) + ? (`Ad Unit: ${bidOrAdUnit.code} has multiple label operators. Using the first declared operator: ${labelsFound[0]}`) + : (`Bidder: ${bidOrAdUnit.bidder} in Ad Unit: ${adUnitCode} has multiple label operators. Using the first declared operator: ${labelsFound[0]}`)}`); + } + labelOperator = labelsFound[0]; + + if (labelOperator === 'labelAll' && Array.isArray(bidOrAdUnit[labelOperator])) { + if (bidOrAdUnit.labelAll.length === 0) { + utils.logWarn(`SizeMappingV2:: Ad Unit: ${bidOrAdUnit.code} has declared property labelAll with an empty array. Ad Unit is still enabled!`); + return true; + } + return bidOrAdUnit.labelAll.every(label => includes(activeLabels, label)); + } else if (labelOperator === 'labelAny' && Array.isArray(bidOrAdUnit[labelOperator])) { + if (bidOrAdUnit.labelAny.length === 0) { + utils.logWarn(`SizeMappingV2:: Ad Unit: ${bidOrAdUnit.code} has declared property labelAny with an empty array. Ad Unit is still enabled!`); + return true; + } + return bidOrAdUnit.labelAny.some(label => includes(activeLabels, label)); + } + return true; +} + +/** + * Processes the MediaTypes object and calculates the active size buckets for each Media Type. Uses `window.innerWidth` and `window.innerHeight` + * to calculate the width and height of the active Viewport. + * @param {MediaTypes} mediaTypes Contains information about supported media types for an Ad Unit and size information for each of those types + * @returns {FilteredMediaTypes} Filtered mediaTypes object with relevant media types filtered by size buckets based on activeViewPort size + */ +export function getFilteredMediaTypes(mediaTypes) { + let + activeViewportWidth, + activeViewportHeight, + transformedMediaTypes; + + transformedMediaTypes = utils.deepClone(mediaTypes); + + let activeSizeBucket = { + banner: undefined, + video: undefined, + native: undefined + } + + try { + activeViewportWidth = utils.getWindowTop().innerWidth; + activeViewportHeight = utils.getWindowTop().innerHeight; + } catch (e) { + utils.logWarn(`SizeMappingv2:: Unfriendly iframe blocks viewport size to be evaluated correctly`); + activeViewportWidth = window.innerWidth; + activeViewportHeight = window.innerHeight; + } + const activeViewport = [activeViewportWidth, activeViewportHeight]; + Object.keys(mediaTypes).map(mediaType => { + const sizeConfig = mediaTypes[mediaType].sizeConfig; + if (sizeConfig) { + activeSizeBucket[mediaType] = getActiveSizeBucket(sizeConfig, activeViewport); + const filteredSizeConfig = sizeConfig.filter(config => config.minViewPort === activeSizeBucket[mediaType] && isSizeConfigActivated(mediaType, config)); + transformedMediaTypes[mediaType] = Object.assign({ filteredSizeConfig }, mediaTypes[mediaType]); + + // transform mediaTypes object + const config = { + banner: 'sizes', + video: 'playerSize' + }; + + if (transformedMediaTypes[mediaType].filteredSizeConfig.length > 0) { + // map sizes or playerSize property in filteredSizeConfig object to transformedMediaTypes.banner.sizes if mediaType is banner + // or transformedMediaTypes.video.playerSize if the mediaType in video. + // doesn't apply to native mediaType since native doesn't have any property defining 'sizes' or 'playerSize'. + if (mediaType !== 'native') { + transformedMediaTypes[mediaType][config[mediaType]] = transformedMediaTypes[mediaType].filteredSizeConfig[0][config[mediaType]]; + } + } else { + delete transformedMediaTypes[mediaType]; + } + } + }) + + // filter out 'undefined' values from activeSizeBucket object and attach sizes/playerSize information against the active size bucket. + const sizeBucketToSizeMap = Object + .keys(activeSizeBucket) + .filter(mediaType => activeSizeBucket[mediaType] !== undefined) + .reduce((sizeBucketToSizeMap, mediaType) => { + sizeBucketToSizeMap[mediaType] = { + activeSizeBucket: activeSizeBucket[mediaType], + activeSizeDimensions: (mediaType === 'banner') ? ( + // banner mediaType gets deleted incase no sizes are specified for a given size bucket, that's why this check is necessary + (transformedMediaTypes.banner) ? (transformedMediaTypes.banner.sizes) : ([]) + ) : ((mediaType === 'video') ? ( + // video mediaType gets deleted incase no playerSize is specified for a given size bucket, that's why this check is necessary + (transformedMediaTypes.video) ? (transformedMediaTypes.video.playerSize) : ([]) + ) : ('NA')) + }; + return sizeBucketToSizeMap; + }, {}); + + return { mediaTypes, sizeBucketToSizeMap, activeViewport, transformedMediaTypes }; +}; + +/** + * Evaluates the given sizeConfig object and checks for various properties to determine if the sizeConfig is active or not. For example, + * let's suppose the sizeConfig is for a Banner media type. Then, if the sizes property is found empty, it returns false, else returns true. + * In case of a Video media type, it checks the playerSize property. If found empty, returns false, else returns true. + * In case of a Native media type, it checks the active property. If found false, returns false, if found true, returns true. + * @param {string} mediaType It can be 'banner', 'native' or 'video' + * @param {Object} sizeConfig Represents the sizeConfig object which is active based on the current viewport size + * @returns {boolean} Represents if the size config is active or not + */ +export function isSizeConfigActivated(mediaType, sizeConfig) { + switch (mediaType) { + case 'banner': + // we need this check, sizeConfig.sizes[0].length > 0, in place because a sizeBucket can have sizes: [], + // gets converted to sizes: [[]] in the checkAdUnitSetupHook function + return sizeConfig.sizes && sizeConfig.sizes.length > 0 && sizeConfig.sizes[0].length > 0; + case 'video': + // for why we need the last check, read the above comment + return sizeConfig.playerSize && sizeConfig.playerSize.length > 0 && sizeConfig.playerSize[0].length > 0; + case 'native': + return sizeConfig.active; + default: + return false; + } +} + +/** + * Returns the active size bucket for a given media type + * @param {Array} sizeConfig SizeConfig defines the characteristics of an Ad Unit categorised into multiple size buckets per media type + * @param {Array} activeViewport Viewport size of the browser in the form [w, h] (w -> width, h -> height) + * Calculated at the time of making call to pbjs.requestBids function + * @returns {Array} The active size bucket matching the activeViewPort, for example: [750, 0] + */ +export function getActiveSizeBucket(sizeConfig, activeViewport) { + let activeSizeBucket = []; + sizeConfig + .sort((a, b) => a.minViewPort[0] - b.minViewPort[0]) + .forEach(config => { + if (activeViewport[0] >= config.minViewPort[0]) { + if (activeViewport[1] >= config.minViewPort[1]) { + activeSizeBucket = config.minViewPort; + } else { + activeSizeBucket = []; + } + } + }) + return activeSizeBucket; +} + +export function getRelevantMediaTypesForBidder(sizeConfig, activeViewport) { + if (internal.checkBidderSizeConfigFormat(sizeConfig)) { + const activeSizeBucket = internal.getActiveSizeBucket(sizeConfig, activeViewport); + return sizeConfig.filter(config => config.minViewPort === activeSizeBucket)[0]['relevantMediaTypes']; + } + return []; +} + +// sets sizeMappingInternalStore for a given auctionId with relevant adUnit information returned from the call to 'getFilteredMediaTypes' function +// returns adUnit details object. +export function getAdUnitDetail(auctionId, adUnit) { + const adUnitDetail = sizeMappingInternalStore.getAuctionDetail(auctionId).adUnits.filter(adUnitDetail => adUnitDetail.adUnitCode === adUnit.code); + + if (adUnitDetail.length > 0) { + return adUnitDetail[0]; + } else { + const { mediaTypes, sizeBucketToSizeMap, activeViewport, transformedMediaTypes } = internal.getFilteredMediaTypes(adUnit.mediaTypes); + + const adUnitDetail = { + adUnitCode: adUnit.code, + mediaTypes, + sizeBucketToSizeMap, + activeViewport, + transformedMediaTypes + }; + + // 'filteredMediaTypes' are the mediaTypes that got removed/filtered-out from adUnit.mediaTypes after sizeConfig filtration. + const filteredMediaTypes = Object.keys(mediaTypes).filter(mt => Object.keys(transformedMediaTypes).indexOf(mt) === -1) + + utils.logInfo(`SizeMappingV2:: AdUnit: ${adUnit.code} - Active size buckets after filtration: `, sizeBucketToSizeMap); + if (filteredMediaTypes.length > 0) { + utils.logInfo(`SizeMappingV2:: AdUnit: ${adUnit.code} - mediaTypes that got filtered out: ${filteredMediaTypes}`); + } + + // set adUnitDetail in sizeMappingInternalStore against the correct 'auctionId'. + sizeMappingInternalStore.setAuctionDetail(auctionId, adUnitDetail); + return adUnitDetail; + } +} + +export function getBids({ bidderCode, auctionId, bidderRequestId, adUnits, labels, src }) { + return adUnits.reduce((result, adUnit) => { + if (internal.isLabelActivated(adUnit, labels, adUnit.code)) { + if (adUnit.mediaTypes && utils.isValidMediaTypes(adUnit.mediaTypes)) { + const { activeViewport, transformedMediaTypes } = internal.getAdUnitDetail(auctionId, adUnit); + + // check if adUnit has any active media types remaining, if not drop the adUnit from auction, + // else proceed to evaluate the bids object. + if (Object.keys(transformedMediaTypes).length === 0) { + utils.logInfo(`SizeMappingV2:: Ad Unit: ${adUnit.code} is disabled since there are no active media types after sizeConfig filtration.`); + return result; + } + result + .push(adUnit.bids.filter(bid => bid.bidder === bidderCode) + .reduce((bids, bid) => { + if (internal.isLabelActivated(bid, labels, adUnit.code)) { + // handle native params + const nativeParams = adUnit.nativeParams || utils.deepAccess(adUnit, 'mediaTypes.native'); + if (nativeParams) { + bid = Object.assign({}, bid, { + nativeParams: processNativeAdUnitParams(nativeParams) + }); + } + + bid = Object.assign({}, bid, utils.getDefinedParams(adUnit, ['mediaType', 'renderer'])); + + if (bid.sizeConfig) { + const relevantMediaTypes = internal.getRelevantMediaTypesForBidder(bid.sizeConfig, activeViewport); + if (relevantMediaTypes.length === 0) { + utils.logError(`SizeMappingV2:: AdUnit: ${adUnit.code}, Bidder: ${bidderCode} - sizeConfig is not configured properly. This bidder won't be eligible for sizeConfig checks and will remail active.`); + bid = Object.assign({}, bid); + } else if (relevantMediaTypes[0] !== 'none') { + const bidderMediaTypes = Object + .keys(transformedMediaTypes) + .filter(mt => relevantMediaTypes.indexOf(mt) > -1) + .reduce((mediaTypes, mediaType) => { + mediaTypes[mediaType] = transformedMediaTypes[mediaType]; + return mediaTypes; + }, {}); + + if (Object.keys(bidderMediaTypes).length > 0) { + bid = Object.assign({}, bid, { mediaTypes: bidderMediaTypes }); + } else { + utils.logInfo(`SizeMappingV2:: AdUnit: ${adUnit.code}, Bidder: ${bid.bidder} - 'relevantMediaTypes' for this bidder does not match with any of the active mediaTypes at the Ad Unit level. This bidder is disabled.`); + return bids; + } + } else { + utils.logInfo(`SizeMappingV2:: AdUnit: ${adUnit.code}, Bidder: ${bid.bidder} - 'relevantMediaTypes' is set to 'none' in sizeConfig for current viewport size. This bidder is disabled.`); + return bids; + } + } + bids.push(Object.assign({}, bid, { + adUnitCode: adUnit.code, + transactionId: adUnit.transactionId, + sizes: utils.deepAccess(transformedMediaTypes, 'banner.sizes') || utils.deepAccess(transformedMediaTypes, 'video.playerSize') || [], + mediaTypes: bid.mediaTypes || transformedMediaTypes, + bidId: bid.bid_id || utils.getUniqueIdentifierStr(), + bidderRequestId, + auctionId, + src, + bidRequestsCount: adunitCounter.getRequestsCounter(adUnit.code), + bidderRequestsCount: adunitCounter.getBidderRequestsCounter(adUnit.code, bid.bidder), + bidderWinsCount: adunitCounter.getBidderWinsCounter(adUnit.code, bid.bidder) + })); + return bids; + } else { + utils.logInfo(`SizeMappingV2:: AdUnit: ${adUnit.code}, Bidder: ${bid.bidder} - Label check for this bidder has failed. This bidder is disabled.`); + return bids; + } + }, [])); + } else { + utils.logWarn(`SizeMappingV2:: Ad Unit: ${adUnit.code} has declared invalid mediaTypes or has not declared a mediaTypes property`); + } + } else { + utils.logInfo(`SizeMappingV2:: Ad Unit: ${adUnit.code} is disabled due to failing label check.`); + return result; + } + return result; + }, []).reduce(utils.flatten, []).filter(val => val !== ''); +} diff --git a/src/adapterManager.js b/src/adapterManager.js index 134c21e5d7c..9886934ab47 100644 --- a/src/adapterManager.js +++ b/src/adapterManager.js @@ -116,6 +116,8 @@ function getBids({bidderCode, auctionId, bidderRequestId, adUnits, labels, src}) }, []).reduce(flatten, []).filter(val => val !== ''); } +const hookedGetBids = hook('sync', getBids, 'getBids'); + function getAdUnitCopyForPrebidServer(adUnits) { let adaptersServerSide = _s2sConfig.bidders; let adUnitsCopy = utils.deepClone(adUnits); @@ -229,7 +231,7 @@ adapterManager.makeBidRequests = hook('sync', function (adUnits, auctionStart, a auctionId, bidderRequestId, tid, - bids: getBids({bidderCode, auctionId, bidderRequestId, 'adUnits': utils.deepClone(adUnitsS2SCopy), labels, src: CONSTANTS.S2S.SRC}), + bids: hookedGetBids({bidderCode, auctionId, bidderRequestId, 'adUnits': utils.deepClone(adUnitsS2SCopy), labels, src: CONSTANTS.S2S.SRC}), auctionStart: auctionStart, timeout: _s2sConfig.timeout, src: CONSTANTS.S2S.SRC, @@ -264,7 +266,7 @@ adapterManager.makeBidRequests = hook('sync', function (adUnits, auctionStart, a bidderCode, auctionId, bidderRequestId, - bids: getBids({bidderCode, auctionId, bidderRequestId, 'adUnits': utils.deepClone(adUnitsClientCopy), labels, src: 'client'}), + bids: hookedGetBids({bidderCode, auctionId, bidderRequestId, 'adUnits': utils.deepClone(adUnitsClientCopy), labels, src: 'client'}), auctionStart: auctionStart, timeout: cbTimeout, refererInfo diff --git a/src/prebid.js b/src/prebid.js index e9fd0b299a8..aa2d2493e7b 100644 --- a/src/prebid.js +++ b/src/prebid.js @@ -68,20 +68,74 @@ function setRenderSize(doc, width, height) { } } -export const checkAdUnitSetup = hook('sync', function (adUnits) { - function validateSizes(sizes, targLength) { - let cleanSizes = []; - if (utils.isArray(sizes) && ((targLength) ? sizes.length === targLength : sizes.length > 0)) { - // check if an array of arrays or array of numbers - if (sizes.every(sz => isArrayOfNums(sz, 2))) { - cleanSizes = sizes; - } else if (isArrayOfNums(sizes, 2)) { - cleanSizes.push(sizes); - } +function validateSizes(sizes, targLength) { + let cleanSizes = []; + if (utils.isArray(sizes) && ((targLength) ? sizes.length === targLength : sizes.length > 0)) { + // check if an array of arrays or array of numbers + if (sizes.every(sz => isArrayOfNums(sz, 2))) { + cleanSizes = sizes; + } else if (isArrayOfNums(sizes, 2)) { + cleanSizes.push(sizes); + } + } + return cleanSizes; +} + +function validateBannerMediaType(adUnit) { + const banner = adUnit.mediaTypes.banner; + const bannerSizes = validateSizes(banner.sizes); + if (bannerSizes.length > 0) { + banner.sizes = bannerSizes; + // Deprecation Warning: This property will be deprecated in next release in favor of adUnit.mediaTypes.banner.sizes + adUnit.sizes = bannerSizes; + } else { + utils.logError('Detected a mediaTypes.banner object without a proper sizes field. Please ensure the sizes are listed like: [[300, 250], ...]. Removing invalid mediaTypes.banner object from request.'); + delete adUnit.mediaTypes.banner + } +} + +function validateVideoMediaType(adUnit) { + const video = adUnit.mediaTypes.video; + let tarPlayerSizeLen = (typeof video.playerSize[0] === 'number') ? 2 : 1; + + const videoSizes = validateSizes(video.playerSize, tarPlayerSizeLen); + if (videoSizes.length > 0) { + if (tarPlayerSizeLen === 2) { + utils.logInfo('Transforming video.playerSize from [640,480] to [[640,480]] so it\'s in the proper format.'); } - return cleanSizes; + video.playerSize = videoSizes; + // Deprecation Warning: This property will be deprecated in next release in favor of adUnit.mediaTypes.video.playerSize + adUnit.sizes = videoSizes; + } else { + utils.logError('Detected incorrect configuration of mediaTypes.video.playerSize. Please specify only one set of dimensions in a format like: [[640, 480]]. Removing invalid mediaTypes.video.playerSize property from request.'); + delete adUnit.mediaTypes.video.playerSize; + } +} + +function validateNativeMediaType(adUnit) { + const native = adUnit.mediaTypes.native; + if (native.image && native.image.sizes && !Array.isArray(native.image.sizes)) { + utils.logError('Please use an array of sizes for native.image.sizes field. Removing invalid mediaTypes.native.image.sizes property from request.'); + delete adUnit.mediaTypes.native.image.sizes; + } + if (native.image && native.image.aspect_ratios && !Array.isArray(native.image.aspect_ratios)) { + utils.logError('Please use an array of sizes for native.image.aspect_ratios field. Removing invalid mediaTypes.native.image.aspect_ratios property from request.'); + delete adUnit.mediaTypes.native.image.aspect_ratios; } + if (native.icon && native.icon.sizes && !Array.isArray(native.icon.sizes)) { + utils.logError('Please use an array of sizes for native.icon.sizes field. Removing invalid mediaTypes.native.icon.sizes property from request.'); + delete adUnit.mediaTypes.native.icon.sizes; + } +} +export const adUnitSetupChecks = { + validateBannerMediaType, + validateVideoMediaType, + validateNativeMediaType, + validateSizes +}; + +export const checkAdUnitSetup = hook('sync', function (adUnits) { return adUnits.filter(adUnit => { const mediaTypes = adUnit.mediaTypes; if (!mediaTypes || Object.keys(mediaTypes).length === 0) { @@ -90,45 +144,18 @@ export const checkAdUnitSetup = hook('sync', function (adUnits) { } if (mediaTypes.banner) { - const bannerSizes = validateSizes(mediaTypes.banner.sizes); - if (bannerSizes.length > 0) { - mediaTypes.banner.sizes = bannerSizes; - // TODO eventually remove this internal copy once we're ready to deprecate bidders from reading this adUnit.sizes property - adUnit.sizes = bannerSizes; - } else { - utils.logError('Detected a mediaTypes.banner object without a proper sizes field. Please ensure the sizes are listed like: [[300, 250], ...]. Removing invalid mediaTypes.banner object from request.'); - delete adUnit.mediaTypes.banner; - } + validateBannerMediaType(adUnit); } if (mediaTypes.video) { const video = mediaTypes.video; if (video.playerSize) { - let tarPlayerSizeLen = (typeof video.playerSize[0] === 'number') ? 2 : 1; - const videoSizes = validateSizes(video.playerSize, tarPlayerSizeLen); - if (videoSizes.length > 0) { - adUnit.sizes = video.playerSize = videoSizes; - } else { - utils.logError('Detected incorrect configuration of mediaTypes.video.playerSize. Please specify only one set of dimensions in a format like: [[640, 480]]. Removing invalid mediaTypes.video.playerSize property from request.'); - delete adUnit.mediaTypes.video.playerSize; - } + validateVideoMediaType(adUnit); } } if (mediaTypes.native) { - const nativeObj = mediaTypes.native; - if (nativeObj.image && nativeObj.image.sizes && !Array.isArray(nativeObj.image.sizes)) { - utils.logError('Please use an array of sizes for native.image.sizes field. Removing invalid mediaTypes.native.image.sizes property from request.'); - delete adUnit.mediaTypes.native.image.sizes; - } - if (nativeObj.image && nativeObj.image.aspect_ratios && !Array.isArray(nativeObj.image.aspect_ratios)) { - utils.logError('Please use an array of sizes for native.image.aspect_ratios field. Removing invalid mediaTypes.native.image.aspect_ratios property from request.'); - delete adUnit.mediaTypes.native.image.aspect_ratios; - } - if (nativeObj.icon && nativeObj.icon.sizes && !Array.isArray(nativeObj.icon.sizes)) { - utils.logError('Please use an array of sizes for native.icon.sizes field. Removing invalid mediaTypes.native.icon.sizes property from request.'); - delete adUnit.mediaTypes.native.icon.sizes; - } + validateNativeMediaType(adUnit); } return true; }); diff --git a/test/spec/modules/sizeMappingV2_spec.js b/test/spec/modules/sizeMappingV2_spec.js new file mode 100644 index 00000000000..4b76ce2fa1c --- /dev/null +++ b/test/spec/modules/sizeMappingV2_spec.js @@ -0,0 +1,1580 @@ +import { expect } from 'chai'; +import * as utils from '../../../src/utils.js'; + +import { + isUsingNewSizeMapping, + checkAdUnitSetupHook, + checkBidderSizeConfigFormat, + isLabelActivated, + isSizeConfigActivated, + getActiveSizeBucket, + getRelevantMediaTypesForBidder, + sizeMappingInternalStore, + getAdUnitDetail, + getFilteredMediaTypes, + getBids, + internal +} from '../../../modules/sizeMappingV2.js'; + +import { adUnitSetupChecks } from '../../../src/prebid.js'; + +const AD_UNITS = [{ + code: 'div-gpt-ad-1460505748561-0', + mediaTypes: { + banner: { + sizeConfig: [ + { minViewPort: [0, 0], sizes: [] }, // remove if < 750px + { minViewPort: [750, 0], sizes: [[300, 250], [300, 600]] }, // between 750px and 1199px + { minViewPort: [1200, 0], sizes: [[970, 90], [728, 90], [300, 250]] }, // between 1200px and 1599px + { minViewPort: [1600, 0], sizes: [[1000, 300], [970, 90], [728, 90], [300, 250]] } // greater than 1600px + ] + }, + video: { + context: 'instream', + sizeConfig: [ + { minViewPort: [0, 0], playerSize: [] }, + { minViewPort: [800, 0], playerSize: [[640, 400]] }, + { minViewPort: [1200, 0], playerSize: [] } + ] + }, + native: { + image: { + required: true, + sizes: [150, 50] + }, + title: { + required: true, + len: 80 + }, + sponsoredBy: { + required: true + }, + clickUrl: { + required: true + }, + privacyLink: { + required: false + }, + body: { + required: true + }, + icon: { + required: true, + sizes: [50, 50] + }, + sizeConfig: [ + { minViewPort: [0, 0], active: false }, + { minViewPort: [600, 0], active: true }, + { minViewPort: [1000, 0], active: false } + ] + } + }, + bids: [{ + bidder: 'appnexus', + params: { + placementId: 13144370 + } + }] +}, { + code: 'div-gpt-ad-1460505748561-1', + mediaTypes: { + banner: { + sizes: [[300, 250], [300, 600]] + }, + video: { + context: 'instream', + playerSize: [300, 460] + } + }, + bids: [{ + bidder: 'appnexus', + params: { + placementId: 13144370 + } + }, { + bidder: 'rubicon', + params: { + accountId: 14062, + siteId: 70608, + zoneId: 498816 + }, + sizeConfig: [ + { minViewPort: [0, 0], relevantMediaTypes: ['none'] }, + { minViewPort: [800, 0], relevantMediaTypes: ['banner'] }, + { minViewPort: [1600, 0], relevantMediaTypes: ['none'] } + ] + }] +}]; + +// deletes the sizeConfig property from the adUnits either at the Ad Unit level or at the Bids level or at both. +function deleteSizeConfig(adUnits, config) { + const { adUnitLevel, bidsLevel } = config; + adUnits.forEach(adUnit => { + const mediaTypes = adUnit.mediaTypes; + if (adUnitLevel) { + Object.keys(mediaTypes).forEach(mediaType => { + if (mediaTypes[mediaType].sizeConfig) { + delete adUnit.mediaTypes[mediaType].sizeConfig; + if (mediaType === 'banner') { + adUnit.mediaTypes[mediaType].sizes = [[300, 600], [300, 200]]; + } else if (mediaType === 'video') { + adUnit.mediaTypes[mediaType].playerSize = [[640, 400]]; + } + } + }); + } + + if (bidsLevel) { + adUnit.bids.forEach(bid => { + if (bid.sizeConfig) { + delete bid.sizeConfig; + } + }); + } + }); + return adUnits; +} + +describe('sizeMappingV2', function () { + describe('isUsingNewSizeMaping(adUntis, auctionId)', function () { + it('should return "false" if sizeConfig is not declared both at the adUnits level and the bids level', function () { + let adUnits = utils.deepClone(AD_UNITS); + + // delete the sizeConfig property from AD_UNITS object at the Ad Unit and the Bids level both. + adUnits = deleteSizeConfig(adUnits, { adUnitLevel: true, bidsLevel: true }); + const usingNewSizeMappingBool = isUsingNewSizeMapping(adUnits); + + // isUsingNewSizeMapping should return false because sizeConfig object is not present both at the Ad Units and the Bids level + // for all the adUnits that are checked. + expect(usingNewSizeMappingBool).to.be.false; + }); + + it('should return "true" if sizeConfig is declared at the adUnits level but not at the bids level', function () { + let adUnits = utils.deepClone(AD_UNITS); + + // delete the sizeConfig property from AD_UNITS object at the Bids level but not at the Ad Unit level. + adUnits = deleteSizeConfig(adUnits, { adUnitLevel: false, bidsLevel: true }); + const usingNewSizeMappingBool = isUsingNewSizeMapping(adUnits); + + // isUsingNewSizeMapping should return true because sizeConfig object is present at the Ad Units level but not at the + // Bids level. + expect(usingNewSizeMappingBool).to.be.true; + }); + + it('should return "true" if sizeConfig is declared at the bids level but not at the adUnits level', function () { + let adUnits = utils.deepClone(AD_UNITS); + + // delete the sizeConfig property from AD_UNITS object at the Ad Unit level but not at the Bids level. + adUnits = deleteSizeConfig(adUnits, { adUnitLevel: true, bidsLevel: false }); + const usingNewSizeMappingBool = isUsingNewSizeMapping(adUnits); + + // isUsingNewSizeMapping should return true because sizeConfig object is present at the Bids level but not at the + // Ad Unit level. + expect(usingNewSizeMappingBool).to.be.true; + }); + + it('should return "true" if sizeConfig is declared both at the adUnits level and at the bids level', function () { + let adUnits = utils.deepClone(AD_UNITS); + + const usingNewSizeMappingBool = isUsingNewSizeMapping(adUnits); + + // isUsingNewSizeMapping should return true because sizeConfig object is present both at the Ad Unit level and at the + // Bids level. + expect(usingNewSizeMappingBool).to.be.true; + }); + }); + + describe('checkAdUnitSetupHook(adUnits)', function () { + beforeEach(function () { + sinon.spy(utils, 'logError'); + }); + + afterEach(function () { + utils.logError.restore(); + }); + + it('should filter out adUnit if it does not contain the required property mediaTypes', function () { + let adUnits = utils.deepClone(AD_UNITS); + delete adUnits[0].mediaTypes; + // before checkAdUnitSetupHook is called, the length of adUnits should be '2' + expect(adUnits.length).to.equal(2); + adUnits = checkAdUnitSetupHook(adUnits); + + // after checkAdUnitSetupHook is called, the length of adUnits should be '1' + expect(adUnits.length).to.equal(1); + expect(adUnits[0].code).to.equal('div-gpt-ad-1460505748561-1'); + }); + + it('should filter out adUnit if it has declared property mediaTypes with an empty object', function () { + let adUnits = utils.deepClone(AD_UNITS); + adUnits[0].mediaTypes = {}; + // before checkAdUnitSetupHook is called, the length of adUnits should be '2' + expect(adUnits.length).to.equal(2); + adUnits = checkAdUnitSetupHook(adUnits); + + // after checkAdUnitSetupHook is called, the length of adUnits should be '1' + expect(adUnits.length).to.equal(1); + expect(adUnits[0].code).to.equal('div-gpt-ad-1460505748561-1'); + }); + + it('should log an error message if Ad Unit does not contain the required property "mediaTypes"', function () { + let adUnits = utils.deepClone(AD_UNITS); + delete adUnits[0].mediaTypes; + + checkAdUnitSetupHook(adUnits); + sinon.assert.callCount(utils.logError, 1); + sinon.assert.calledWith(utils.logError, 'Detected adUnit.code \'div-gpt-ad-1460505748561-0\' did not have a \'mediaTypes\' object defined. This is a required field for the auction, so this adUnit has been removed.'); + }); + + describe('banner mediaTypes checks', function () { + beforeEach(function () { + sinon.spy(adUnitSetupChecks, 'validateBannerMediaType'); + }); + + afterEach(function () { + adUnitSetupChecks.validateBannerMediaType.restore(); + }); + + it('should delete banner mediaType if it does not constain sizes or sizeConfig property', function () { + let adUnits = utils.deepClone(AD_UNITS); + delete adUnits[0].mediaTypes.banner.sizeConfig; + + // before checkAdUnitSetupHook is called, adUnits[0].mediaTypes.banner property should exist. + expect(adUnits[0].mediaTypes).to.have.property('banner'); + + adUnits = checkAdUnitSetupHook(adUnits); + + // after checkAdUnitSetupHook is called, adUnits[0].mediaTypes.banner property should not exist. + expect(adUnits[0].mediaTypes).to.not.have.property('banner'); + }); + + it('should log an error message if mediaTypes.banner does not contain "sizes" or "sizeConfig" property', function () { + let adUnits = utils.deepClone(AD_UNITS); + // deleteing the sizeConfig property from the first ad unit. + delete adUnits[0].mediaTypes.banner.sizeConfig; + + checkAdUnitSetupHook(adUnits); + sinon.assert.callCount(utils.logError, 1); + sinon.assert.calledWith(utils.logError, 'Detected a mediaTypes.banner object did not include required property sizes or sizeConfig. Removing invalid mediaTypes.banner object from Ad Unit.'); + }); + + it('should call function "validateBannerMediaType" if mediaTypes.sizes is present', function () { + const adUnits = utils.deepClone(AD_UNITS); + checkAdUnitSetupHook(adUnits); + + // since the second Ad Unit in AD_UNITS array uses mediaTypes.sizes, it should get called only once. + sinon.assert.callCount(adUnitSetupChecks.validateBannerMediaType, 1); + sinon.assert.calledWith(adUnitSetupChecks.validateBannerMediaType, adUnits[1]); + }); + + it('should delete mediaTypes.banner object if it\'s property sizeConfig is not declared as an array', function () { + const adUnits = utils.deepClone(AD_UNITS); + const badSizeConfig = { + minViewPort: [0, 0], sizes: [300, 400] + }; + adUnits[0].mediaTypes.banner.sizeConfig = badSizeConfig; + + // before calling checkAdUnitSetupHook(adUnits), mediaTypes.banner should be defined + expect(adUnits[0].mediaTypes).to.have.property('banner'); + + const validatedAdUnits = checkAdUnitSetupHook(adUnits); + + // after calling checkAdUnitSetupHook(adUnits), mediaTypes.banner property should be deleted + expect(validatedAdUnits.length).to.not.have.property('banner'); + }); + + it('should log an error message if sizeConfig property in mediaTypes.banner object is not declared as an array', function () { + const adUnits = utils.deepClone(AD_UNITS); + // badSizeConfig is NOT defined as an Array + const badSizeConfig = { + minViewPort: [0, 0], sizes: [300, 400] + }; + adUnits[0].mediaTypes.banner.sizeConfig = badSizeConfig; + + checkAdUnitSetupHook(adUnits); + sinon.assert.callCount(utils.logError, 1); + sinon.assert.calledWith(utils.logError, `Ad Unit: div-gpt-ad-1460505748561-0: mediaTypes.banner.sizeConfig is NOT an Array. Removing the invalid object mediaTypes.banner from Ad Unit.`); + }); + + it('should delete mediaTypes.banner object if it\'s property sizeConfig does not contain the required properties "minViewPort" and "sizes"', function () { + const adUnits = utils.deepClone(AD_UNITS); + + // badSizeConfig[2] does not contain the required "sizes" property + const badSizeConfig = [ + { minViewPort: [0, 0], sizes: [] }, + { minViewPort: [700, 0], sizes: [[300, 250], [300, 600]] }, + { minViewPort: [1200, 0] } + ]; + adUnits[0].mediaTypes.banner.sizeConfig = badSizeConfig; + + // before calling checkAdUnitSetupHook(adUnits), mediaTypes.banner should be defined + expect(adUnits[0].mediaTypes).to.have.property('banner'); + + const validatedAdUnits = checkAdUnitSetupHook(adUnits); + + // after calling checkAdUnitSetupHook(adUnits), mediaTypes.banner property should be deleted + expect(validatedAdUnits.length).to.not.have.property('banner'); + }); + + it('should log an error message if sizeConfig property in mediaTypes.banner object does not contain the required properties "minViewPort" and "sizes"', function () { + const adUnits = utils.deepClone(AD_UNITS); + + // badSizeConfig[2] does not contain the required "sizes" property + const badSizeConfig = [ + { minViewPort: [0, 0], sizes: [] }, + { minViewPort: [700, 0], sizes: [[300, 250], [300, 600]] }, + { minViewPort: [1200, 0] } + ]; + adUnits[0].mediaTypes.banner.sizeConfig = badSizeConfig; + + checkAdUnitSetupHook(adUnits); + sinon.assert.callCount(utils.logError, 1); + sinon.assert.calledWith(utils.logError, `Ad Unit: div-gpt-ad-1460505748561-0: mediaTypes.banner.sizeConfig[2] is missing required property minViewPort or sizes or both.`); + }); + + it('should delete mediaTypes.banner object if it\'s property sizeConfig has declared minViewPort property which is NOT an Array of two integers', function () { + const adUnits = utils.deepClone(AD_UNITS); + + // badSizeConfig[0].minViewPort is an Array of ONE inteter. It should be an array of two integers to be valid, like [0, 0] + const badSizeConfig = [ + { minViewPort: [0], sizes: [] }, + { minViewPort: [700, 0], sizes: [[300, 250], [300, 600]] }, + { minViewPort: [1200, 0], sizes: [[900, 700], [1000, 1200]] } + ]; + adUnits[0].mediaTypes.banner.sizeConfig = badSizeConfig; + + // before calling checkAdUnitSetupHook(adUnits), mediaTypes.banner should be defined + expect(adUnits[0].mediaTypes).to.have.property('banner'); + + const validatedAdUnits = checkAdUnitSetupHook(adUnits); + + // after calling checkAdUnitSetupHook(adUnits), mediaTypes.banner property should be deleted + expect(validatedAdUnits.length).to.not.have.property('banner'); + }); + + it('should log an error message if sizeConfig has declared property minViewPort which is not an array of two integers', function () { + const adUnits = utils.deepClone(AD_UNITS); + + // badSizeConfig[0].minViewPort is an Array of ONE inteter. It should be an array of two integers to be valid, like [0, 0] + const badSizeConfig = [ + { minViewPort: [0], sizes: [] }, + { minViewPort: [700, 0], sizes: [[300, 250], [300, 600]] }, + { minViewPort: [1200, 0], sizes: [[900, 700], [1000, 1200]] } + ]; + adUnits[0].mediaTypes.banner.sizeConfig = badSizeConfig; + + checkAdUnitSetupHook(adUnits); + sinon.assert.callCount(utils.logError, 1); + sinon.assert.calledWith(utils.logError, `Ad Unit: div-gpt-ad-1460505748561-0: mediaTypes.banner.sizeConfig[0] has property minViewPort decalared with invalid value. Please ensure minViewPort is an Array and is listed like: [700, 0]. Declaring an empty array is not allowed, instead use: [0, 0].`); + }); + + it('should delete mediaTypes.banner object if it\'s property sizeConfig has declared sizes property which is not in the format, [[vw1, vh1], [vw2, vh2]], where vw is viewport width and vh is viewport height', function () { + const adUnits = utils.deepClone(AD_UNITS); + + // badSizeConfig[1].sizes is not declared in the correct format. It should be an Array of TWO integers. + const badSizeConfig = [ + { minViewPort: [0, 0], sizes: [] }, + { minViewPort: [700, 0], sizes: [300] }, + { minViewPort: [1200, 0], sizes: [[900, 700], [1000, 1200]] } + ]; + adUnits[0].mediaTypes.banner.sizeConfig = badSizeConfig; + + // before calling checkAdUnitSetupHook(adUnits), mediaTypes.banner should be defined + expect(adUnits[0].mediaTypes).to.have.property('banner'); + + const validatedAdUnits = checkAdUnitSetupHook(adUnits); + + // after calling checkAdUnitSetupHook(adUnits), mediaTypes.banner property should be deleted + expect(validatedAdUnits.length).to.not.have.property('banner'); + }); + + it('should log an error message if sizeConfig has declared property sizes which is not in the format, [[vw1, vh1], [vw2, vh2]], where vw is viewport width and vh is viewport height', function () { + const adUnits = utils.deepClone(AD_UNITS); + + // badSizeConfig[1].sizes is not declared in the correct format. It should be an Array of TWO integers. + const badSizeConfig = [ + { minViewPort: [0, 0], sizes: [] }, + { minViewPort: [700, 0], sizes: [300] }, + { minViewPort: [1200, 0], sizes: [[900, 700], [1000, 1200]] } + ]; + adUnits[0].mediaTypes.banner.sizeConfig = badSizeConfig; + + checkAdUnitSetupHook(adUnits); + sinon.assert.callCount(utils.logError, 1); + sinon.assert.calledWith(utils.logError, `Ad Unit: div-gpt-ad-1460505748561-0: mediaTypes.banner.sizeConfig[1] has propery sizes declared with invalid value. Please ensure the sizes are listed like: [[300, 250], ...] or like: [] if no sizes are present for that size bucket.`); + }); + + it('should convert sizeConfig.sizes to an array of array, i.e., [360, 600] to [[360, 600]]', function () { + const adUnits = utils.deepClone(AD_UNITS); + + // badSizeConfig[1].sizes is declared as a Array of integers. Correct way to declare is to have it as an Array of Array of Integers, like, [[300, 250]] + // Although, the system won't throw error, it'll internall convert it to the format, [[300, 250]] + const badSizeConfig = [ + { minViewPort: [0, 0], sizes: [] }, + { minViewPort: [700, 0], sizes: [300, 600] }, + { minViewPort: [1200, 0], sizes: [[900, 700], [1000, 1200]] } + ]; + adUnits[0].mediaTypes.banner.sizeConfig = badSizeConfig; + + const validatedAdUnits = checkAdUnitSetupHook(adUnits); + expect(validatedAdUnits[0].mediaTypes.banner.sizeConfig[1].sizes).to.deep.equal([[300, 600]]); + }); + + it('should allow empty array declaration in sizeConfig.sizes to indicate "No valid sizes for this size bucket", and convert it to an array of array, i.e, [] to [[]]', function () { + const adUnits = utils.deepClone(AD_UNITS); + + const validatedAdUnits = checkAdUnitSetupHook(adUnits); + expect(validatedAdUnits[0].mediaTypes.banner.sizeConfig[0].sizes).to.deep.equal([[]]); + }); + + it('should NOT delete mediaTypes.banner object if sizeConfig object is declared correctly', function () { + const adUnits = utils.deepClone(AD_UNITS); + + // before calling checkAdUnitSetupHook, the mediaTypes.banner object should be present on both the Ad Units. + expect(adUnits[0].mediaTypes).to.have.property('banner'); + expect(adUnits[1].mediaTypes).to.have.property('banner'); + + checkAdUnitSetupHook(adUnits); + + // after calling checkAdUnitSetupHook, the mediaTypes.banner object should still be present for both the Ad Units. + expect(adUnits[0].mediaTypes).to.have.property('banner'); + expect(adUnits[1].mediaTypes).to.have.property('banner'); + }); + }); + + describe('video mediaTypes checks', function () { + beforeEach(function () { + sinon.spy(adUnitSetupChecks, 'validateVideoMediaType'); + }); + + afterEach(function () { + adUnitSetupChecks.validateVideoMediaType.restore(); + }); + + it('should call function "validateVideoMediaType" if mediaTypes.video.playerSize is present in the Ad Unit', function () { + const adUnits = utils.deepClone(AD_UNITS); + + checkAdUnitSetupHook(adUnits); + + // since adUntis[1].mediaTypes.video has defined property "playserSize", it should call function "validateVideoMediaType" only once + sinon.assert.callCount(adUnitSetupChecks.validateVideoMediaType, 1); + sinon.assert.calledWith(adUnitSetupChecks.validateVideoMediaType, adUnits[1]); + }); + + it('should delete mediaTypes.video.sizeConfig property if sizeConfig is not declared as an array', function () { + const adUnits = utils.deepClone(AD_UNITS); + + // badSizeConfig is declared as an object, it should have been an Array. + const badSizeConfig = { + minViewPort: [0, 0], playerSize: [640, 400] + }; + adUnits[0].mediaTypes.video.sizeConfig = badSizeConfig; + + // before calling checkAdUnitSetupHook, mediaTypes.video.sizeConfig property should be defined. + expect(adUnits[0].mediaTypes.video).to.have.property('sizeConfig'); + + const validatedAdUnits = checkAdUnitSetupHook(adUnits); + + // after calling checkAdUnitSetupHook, mediaTypes.video.sizeConfig property should be deleted. + expect(validatedAdUnits[0].mediaTypes.video).to.not.have.property('sizeConfig'); + + // check if correct logError is written to the console. + sinon.assert.callCount(utils.logError, 1); + sinon.assert.calledWith(utils.logError, `Ad Unit: div-gpt-ad-1460505748561-0: mediaTypes.video.sizeConfig is NOT an Array. Removing the invalid property mediaTypes.video.sizeConfig from Ad Unit.`); + }); + + it('should delete mediaTypes.video.sizeConfig property if sizeConfig does not contain the required properties "minViewPort" and "playerSize"', function () { + const adUnits = utils.deepClone(AD_UNITS); + + // badSizeConfig[0] doest not contain the required property "playerSize". + const badSizeConfig = [ + { minViewPort: [0, 0] }, + { minViewPort: [1200, 0], playerSize: [640, 400] } + ]; + adUnits[0].mediaTypes.video.sizeConfig = badSizeConfig; + + // before calling checkAdUnitSetupHook, mediaTypes.video.sizeConfig property should be defined. + expect(adUnits[0].mediaTypes.video).to.have.property('sizeConfig'); + + const validatedAdUnits = checkAdUnitSetupHook(adUnits); + + // after calling checkAdUnitSetupHook, mediaTypes.video.sizeConfig property should be deleted. + expect(validatedAdUnits[0].mediaTypes.video).to.not.have.property('sizeConfig'); + + // check if correct logError is written to the console. + sinon.assert.callCount(utils.logError, 1); + sinon.assert.calledWith(utils.logError, `Ad Unit: div-gpt-ad-1460505748561-0: mediaTypes.video.sizeConfig[0] is missing required property minViewPort or playerSize or both. Removing the invalid property mediaTypes.video.sizeConfig from Ad Unit.`); + }); + + it('should delete mediaTypes.video.sizeConfig property if sizeConfig has declared minViewPort property which is NOT an Array of two integers', function () { + const adUnits = utils.deepClone(AD_UNITS); + + // badSizeConfig[1].minViewPort is an Array of Integers. It should have been an Array of Array of Integers. + const badSizeConfig = [ + { minViewPort: [0, 0], playerSize: [] }, + { minViewPort: [1200], playerSize: [640, 400] } + ]; + adUnits[0].mediaTypes.video.sizeConfig = badSizeConfig; + + // before calling checkAdUnitSetupHook, mediaTypes.video.sizeConfig property should be defined. + expect(adUnits[0].mediaTypes.video).to.have.property('sizeConfig'); + + const validatedAdUnits = checkAdUnitSetupHook(adUnits); + + // after calling checkAdUnitSetupHook, mediaTypes.video.sizeConfig property should be deleted. + expect(validatedAdUnits[0].mediaTypes.video).to.not.have.property('sizeConfig'); + + // check if correct logError is written to the console. + sinon.assert.callCount(utils.logError, 1); + sinon.assert.calledWith(utils.logError, `Ad Unit: div-gpt-ad-1460505748561-0: mediaTypes.video.sizeConfig[1] has property minViewPort decalared with invalid value. Please ensure minViewPort is an Array and is listed like: [700, 0]. Declaring an empty array is not allowed, instead use: [0, 0].`); + }); + + it('should delete mediaTypes.video.sizeConfig property if sizeConfig has declared "playerSize" property which is not in the format, [[vw1, vh1]], where vw is viewport width and vh is viewport height', function () { + const adUnits = utils.deepClone(AD_UNITS); + + // badSizeConfig[0].playerSize property is declared incorrectly. + const badSizeConfig = [ + { minViewPort: [0, 0], playerSize: [600, 400, 500] }, + { minViewPort: [1200, 0], playerSize: [640, 400] } + ]; + adUnits[0].mediaTypes.video.sizeConfig = badSizeConfig; + + // before calling checkAdUnitSetupHook, mediaTypes.video.sizeConfig property should be defined. + expect(adUnits[0].mediaTypes.video).to.have.property('sizeConfig'); + + const validatedAdUnits = checkAdUnitSetupHook(adUnits); + + // after calling checkAdUnitSetupHook, mediaTypes.video.sizeConfig property should be deleted. + expect(validatedAdUnits[0].mediaTypes.video).to.not.have.property('sizeConfig'); + + // check if correct logError is written to the console. + sinon.assert.callCount(utils.logError, 1); + sinon.assert.calledWith(utils.logError, `Ad Unit: div-gpt-ad-1460505748561-0: mediaTypes.video.sizeConfig[0] has propery playerSize declared with invalid value. Please ensure the playerSize is listed like: [640, 480] or like: [] if no playerSize is present for that size bucket.`); + }); + + it('should convert sizeConfig.playerSize to an array of array, i.e., [360, 600] to [[360, 600]]', function () { + const adUnits = utils.deepClone(AD_UNITS); + + // badSizeConfig[] has declared "playerSize" as an Array of Intergers. It should be an Array of Array of Integers, like [[640, 400]]. + // Although, it won't throw an error if you declare it like this, but internally, the system will convert it to the format listed above. + const badSizeConfig = [ + { minViewPort: [0, 0], playerSize: [] }, + { minViewPort: [1200, 0], playerSize: [360, 600] } + ]; + adUnits[0].mediaTypes.video.sizeConfig = badSizeConfig; + + const validatedAdUnits = checkAdUnitSetupHook(adUnits); + + expect(validatedAdUnits[0].mediaTypes.video.sizeConfig[0].playerSize).to.deep.equal([[]]); + expect(validatedAdUnits[0].mediaTypes.video.sizeConfig[1].playerSize).to.deep.equal([[360, 600]]); + }); + + it('should convert mediaTypes.video.playerSize to an array of array, i.e., [360, 600] to [[360, 600]]', function () { + const adUnits = utils.deepClone(AD_UNITS); + + const validatedAdUnits = checkAdUnitSetupHook(adUnits); + + expect(validatedAdUnits[1].mediaTypes.video.playerSize).to.deep.equal([[300, 460]]); + }); + + it('should NOT delete mediaTypes.video.sizeConfig property if sizeConfig property is declared correctly', function () { + const adUnits = utils.deepClone(AD_UNITS); + + // before checkAdUnitSetupHook is called + expect(adUnits[0].mediaTypes.video).to.have.property('sizeConfig'); + + const validatedAdUnits = checkAdUnitSetupHook(adUnits); + + // after checkAdUnitSetupHook is called + expect(validatedAdUnits[0].mediaTypes.video).to.have.property('sizeConfig'); + }); + }); + + describe('native mediaTypes checks', function () { + beforeEach(function () { + sinon.spy(adUnitSetupChecks, 'validateNativeMediaType'); + }); + + afterEach(function () { + adUnitSetupChecks.validateNativeMediaType.restore(); + }); + + it('should call function "validateNativeMediaTypes" if mediaTypes.native is defined', function () { + const adUnits = utils.deepClone(AD_UNITS); + checkAdUnitSetupHook(adUnits); + sinon.assert.callCount(adUnitSetupChecks.validateNativeMediaType, 1); + }); + + it('should delete mediaTypes.native.sizeConfig property if sizeConfig does not contain the required properties "minViewPort" and "active"', function () { + const adUnits = utils.deepClone(AD_UNITS); + // badSizeConfig[1] doesn't include required property "active" + const badSizeConfig = [ + { minViewPort: [0, 0], active: false }, + { minViewPort: [1200, 0] } + ]; + adUnits[0].mediaTypes.native.sizeConfig = badSizeConfig; + expect(adUnits[0].mediaTypes.native).to.have.property('sizeConfig'); + + const validatedAdUnits = checkAdUnitSetupHook(adUnits); + expect(validatedAdUnits[0].mediaTypes.native).to.not.have.property('sizeConfig'); + sinon.assert.callCount(utils.logError, 1); + sinon.assert.calledWith(utils.logError, `Ad Unit: div-gpt-ad-1460505748561-0: mediaTypes.native.sizeConfig is missing required property minViewPort or active or both. Removing the invalid property mediaTypes.native.sizeConfig from Ad Unit.`); + }); + + it('should delete mediaTypes.native.sizeConfig property if sizeConfig[].minViewPort is NOT an array of TWO integers', function () { + const adUnits = utils.deepClone(AD_UNITS); + // badSizeConfig[0].minViewPort is an array of three integers. It should ideally be two integers. + const badSizeConfig = [ + { minViewPort: [0, 0, 0], active: false }, + { minViewPort: [1200, 0], active: true } + ]; + adUnits[0].mediaTypes.native.sizeConfig = badSizeConfig; + expect(adUnits[0].mediaTypes.native).to.have.property('sizeConfig'); + + const validatedAdUnits = checkAdUnitSetupHook(adUnits); + expect(validatedAdUnits[0].mediaTypes.native).to.not.have.property('sizeConfig'); + sinon.assert.callCount(utils.logError, 1); + sinon.assert.calledWith(utils.logError, `Ad Unit: div-gpt-ad-1460505748561-0: mediaTypes.native.sizeConfig has properties minViewPort or active decalared with invalid values. Removing the invalid property mediaTypes.native.sizeConfig from Ad Unit.`); + }); + + it('should delete mediaTypes.native.sizeConfig property if sizeConfig[].active is NOT a Boolean', function () { + const adUnits = utils.deepClone(AD_UNITS); + + // badSizeCofnig[0].active is a String value, it should have been a boolean to be valid. + const badSizeConfig = [ + { minViewPort: [0, 0], active: 'false' }, + { minViewPort: [1200, 0], active: true } + ]; + adUnits[0].mediaTypes.native.sizeConfig = badSizeConfig; + expect(adUnits[0].mediaTypes.native).to.have.property('sizeConfig'); + + const validatedAdUnits = checkAdUnitSetupHook(adUnits); + expect(validatedAdUnits[0].mediaTypes.native).to.not.have.property('sizeConfig'); + sinon.assert.callCount(utils.logError, 1); + sinon.assert.calledWith(utils.logError, `Ad Unit: div-gpt-ad-1460505748561-0: mediaTypes.native.sizeConfig has properties minViewPort or active decalared with invalid values. Removing the invalid property mediaTypes.native.sizeConfig from Ad Unit.`); + }); + + it('should NOT delete mediaTypes.native.sizeConfig property if sizeConfig property is declared correctly', function () { + const adUnits = utils.deepClone(AD_UNITS); + expect(adUnits[0].mediaTypes.native).to.have.property('sizeConfig'); + const validatedAdUnits = checkAdUnitSetupHook(adUnits); + expect(validatedAdUnits[0].mediaTypes.native).to.have.property('sizeConfig'); + }); + }); + }); + + describe('checkBidderSizeConfig(sizeConfig)', function () { + it('should return "false" if sizeConfig is NOT declared as an Array at the Bidder level', function () { + const sizeConfig = { + minViewPort: [600, 0], relevantMediaTypes: ['banner'] + }; + expect(checkBidderSizeConfigFormat(sizeConfig)).to.equal(false); + }); + + it('should return "false" if sizeConfig is declared as an empty Array at the Bidder level', function () { + const sizeConfig = []; + expect(checkBidderSizeConfigFormat(sizeConfig)).to.equal(false); + }); + + it('should return "false" if any of the objects in sizeConfig array has not declared the required properties "minViewPort" and/or "relevantMediaTypes"', function () { + const sizeConfig_1 = [ + { minViewPort: [0, 0], relevantMediaTypes: ['none'] }, + { minViewPort: [800, 0] } + ]; + const sizeConfig_2 = [ + { minViewPort: [0, 0], relevantMediaTypes: ['none'] }, + { relevantMediaTypes: ['banner', 'native'] } + ]; + expect(checkBidderSizeConfigFormat(sizeConfig_1)).to.equal(false); + expect(checkBidderSizeConfigFormat(sizeConfig_2)).to.equal(false); + }); + + it('should return "false" if minViewPort is not declared as an array of two integers', function () { + const sizeConfig_1 = [ + { minViewPort: [], relevantMediaTypes: ['none'] } + ]; + const sizeConfig_2 = [ + { minViewPort: [300, 0, 0], relevantMediaTypes: ['banner'] } + ]; + expect(checkBidderSizeConfigFormat(sizeConfig_1)).to.equal(false); + expect(checkBidderSizeConfigFormat(sizeConfig_2)).to.equal(false); + }); + + it('should return "false" if relevantMediaTypes is NOT an Array of one or more of these values, "none", "banner", "video", "native"', function () { + // declaration of relevantMediaTypes as an empty array is not allowed + const sizeConfig_1 = [ + { minViewPort: [0, 0], relevantMediaTypes: [] } + ]; + // relevantMediaTypes can't be an object. It MUST be declared as an array. + const sizeConfig_2 = [ + { minViewPort: [0, 0], relevantMediaTypes: ['none'] }, + { minViewPort: [1200, 0], relevantMediaTypes: {} } + ]; + // 'none' and 'banner' can't be together. It should either be only 'none' or 'banner' + const sizeConfig_3 = [ + { minViewPort: [0, 0], relevantMediaTypes: ['none', 'banner'] }, + { minViewPort: [1200, 0], relevantMediaTypes: ['banner'] } + ]; + // relevantMediaTypes can only be an array of ['banner', 'video', 'native'] or any one of them + const sizeConfig_4 = [ + { minViewPort: [1200, 0], relevantMediaTypes: ['banner'] }, + { minViewPort: [0, 0], relevantMediaTypes: ['video', 'somethingRandom'] }, + { minViewPort: [1600, 0], relevantMediaTypes: ['native', 'video'] } + + ]; + expect(checkBidderSizeConfigFormat(sizeConfig_1)).to.equal(false); + expect(checkBidderSizeConfigFormat(sizeConfig_2)).to.equal(false); + expect(checkBidderSizeConfigFormat(sizeConfig_3)).to.equal(false); + expect(checkBidderSizeConfigFormat(sizeConfig_4)).to.equal(false); + }); + + it('should return "true" if the sizeConfig object is being configured properly at the Bidder level', function () { + const sizeConfig = [ + { minViewPort: [0, 0], relevantMediaTypes: ['none'] }, + { minViewPort: [600, 0], relevantMediaTypes: ['banner'] }, + { minViewPort: [1200, 0], relevantMediaTypes: ['banner', 'video'] } + ]; + expect(checkBidderSizeConfigFormat(sizeConfig)).to.equal(true); + }); + }); + + describe('isLabelActivated(bidOrAdUnit, activeLabels, adUnitCode)', function () { + const labelAny = ['mobile', 'tablet']; + const labelAll = ['mobile', 'tablet', 'desktop', 'HD-Tv']; + const activeLabels = ['mobile', 'tablet', 'desktop']; + const adUnitCode = 'div-gpt-ad-1460505748561-0'; + + beforeEach(function () { + sinon.spy(utils, 'logWarn'); + }); + + afterEach(function () { + utils.logWarn.restore(); + }); + + it('should throw a warning message if both the label operator, "labelAny"/"labelAll" are configured for an Ad Unit', function () { + const [adUnits] = utils.deepClone(AD_UNITS); + adUnits.labelAny = labelAny; + adUnits.labelAll = labelAll; + + isLabelActivated(adUnits, activeLabels, adUnitCode); + + sinon.assert.callCount(utils.logWarn, 1); + sinon.assert.calledWith(utils.logWarn, `SizeMappingV2:: Ad Unit: div-gpt-ad-1460505748561-0 has multiple label operators. Using the first declared operator: labelAny`); + }); + + it('should throw a warning message if both the label operator, "labelAny"/"labelAll" are configured for an Bidder', function () { + const [adUnits] = utils.deepClone(AD_UNITS); + + adUnits.bids[0].labelAny = labelAny; + adUnits.bids[0].labelAll = labelAll; + + isLabelActivated(adUnits.bids[0], activeLabels, adUnitCode); + + sinon.assert.callCount(utils.logWarn, 1); + sinon.assert.calledWith(utils.logWarn, `SizeMappingV2:: Bidder: appnexus in Ad Unit: div-gpt-ad-1460505748561-0 has multiple label operators. Using the first declared operator: labelAny`); + }); + + it('should give priority to the label operator declared first incase two label operators are found on the same Ad Unit or Bidder', function () { + const [adUnits] = utils.deepClone(AD_UNITS); + adUnits.labelAll = labelAll; + adUnits.labelAny = labelAny; + + // activeAdUnit should be "false" + // 'labelAll' -> ['mobile', 'tablet', 'desktop', 'HD-Tv'] will be given priority since it's declared before 'labelAny' + // since, activeLabels -> ['mobile', 'tablet', 'desktop'], doesn't include 'HD-Tv', 'isLabelActivated' function should return "false" + const activeAdUnit = isLabelActivated(adUnits, activeLabels, adUnitCode); + expect(activeAdUnit).to.equal(false); + + // bidder level check + adUnits.bids[0].labelAny = labelAny; + adUnits.bids[0].labelAll = labelAll; + + // activeBidder should be "true" + // 'labelAny' -> ['mobile', 'tablet'] will be given priority since it's declared before 'labelAll' + // since, activeLabels -> ['mobile', 'tablet', 'desktop'] and matches atleast one element in labelAny array, so, it'll return true + const activeBidder = isLabelActivated(adUnits.bids[0], activeLabels, adUnitCode); + expect(activeBidder).to.equal(true); + }); + + it('should throw a warning log message if "labelAll" operator is declared as an empty array', function () { + const [adUnit] = utils.deepClone(AD_UNITS); + adUnit.labelAll = []; + + // adUnit level check + isLabelActivated(adUnit, activeLabels, adUnitCode); + + sinon.assert.callCount(utils.logWarn, 1); + sinon.assert.calledWith(utils.logWarn, `SizeMappingV2:: Ad Unit: div-gpt-ad-1460505748561-0 has declared property labelAll with an empty array. Ad Unit is still enabled!`); + + // bidder level check + isLabelActivated(adUnit.bids[0], activeLabels, adUnitCode); + + sinon.assert.callCount(utils.logWarn, 1); + sinon.assert.calledWith(utils.logWarn, `SizeMappingV2:: Ad Unit: div-gpt-ad-1460505748561-0 has declared property labelAll with an empty array. Ad Unit is still enabled!`); + }); + + it('should throw a warning log message if "labelAny" operator is declared as an empty array', function () { + const [adUnit] = utils.deepClone(AD_UNITS); + adUnit.labelAny = []; + + // adUnit level check + isLabelActivated(adUnit, activeLabels, adUnitCode); + + sinon.assert.callCount(utils.logWarn, 1); + sinon.assert.calledWith(utils.logWarn, `SizeMappingV2:: Ad Unit: div-gpt-ad-1460505748561-0 has declared property labelAny with an empty array. Ad Unit is still enabled!`); + + // bidder level check + isLabelActivated(adUnit.bids[0], activeLabels, adUnitCode); + sinon.assert.callCount(utils.logWarn, 1); + sinon.assert.calledWith(utils.logWarn, `SizeMappingV2:: Ad Unit: div-gpt-ad-1460505748561-0 has declared property labelAny with an empty array. Ad Unit is still enabled!`); + }); + + it('should return "true" if label operators are not present on the Ad Unit or Bidder', function () { + const [adUnit] = utils.deepClone(AD_UNITS); + + // adUnit level check + const activeAdUnit = isLabelActivated(adUnit, activeLabels, adUnitCode); + expect(activeAdUnit).to.equal(true); + + // bidder level check + const activeBidder = isLabelActivated(adUnit.bids[0], activeLabels, adUnitCode); + expect(activeBidder).to.equal(true); + }); + + it('should filter out the values correctly for the label operators "labelAll"', function () { + const [adUnit] = utils.deepClone(AD_UNITS); + + // adUnit level checks + adUnit.labelAll = labelAll; + + // const labelAll = ['mobile', 'tablet', 'desktop', 'HD-Tv']; + // const activeLabels = ['mobile', 'tablet', 'desktop']; + + const activeAdUnit = isLabelActivated(adUnit, activeLabels, adUnitCode); + expect(activeAdUnit).to.equal(false) + + // bidder level checks + adUnit.bids[0].labelAll = labelAll; + + const activeBidder = isLabelActivated(adUnit.bids[0], activeLabels, adUnitCode); + expect(activeBidder).to.equal(false); + }); + + it('should filter out the values correctly for the label operators "labelAny"', function () { + const [adUnit] = utils.deepClone(AD_UNITS); + + // adUnit level checks + adUnit.labelAny = labelAny; + + // const labelAny = ['mobile', 'tablet']; + // const activeLabels = ['mobile', 'tablet', 'desktop']; + + const activeAdUnit = isLabelActivated(adUnit, activeLabels, adUnitCode); + expect(activeAdUnit).to.equal(true) + + // bidder level checks + adUnit.bids[0].labelAny = labelAny; + + const activeBidder = isLabelActivated(adUnit.bids[0], activeLabels, adUnitCode); + expect(activeBidder).to.equal(true); + }); + }); + + describe('isSizeConfigActivated(mediaType, sizeConfig)', function () { + it('should return "false" for the scenarios where sizeConfig should should not get activated', function () { + // banner test + const sizeConfigBanner = { minViewPort: [0, 0], sizes: [] }; + const bannerActive = isSizeConfigActivated('banner', sizeConfigBanner); + expect(bannerActive).to.equal(false); + + // video test + const sizeConfigVideo = { minViewPort: [0, 0], playerSize: [] }; + const videoActive = isSizeConfigActivated('video', sizeConfigVideo); + expect(videoActive).to.equal(false); + + // native test + const sizeConfigNative = { minViewPort: [0, 0], active: false }; + const nativeActive = isSizeConfigActivated('native', sizeConfigNative); + expect(nativeActive).to.equal(false); + }); + + it('should return "true" for the scenarios where sizeConfig should get activated', function () { + // banner test + const sizeConfigBanner = { minViewPort: [0, 0], sizes: [[300, 600], [970, 1200]] }; + const bannerActive = isSizeConfigActivated('banner', sizeConfigBanner); + expect(bannerActive).to.equal(true); + + // video test + const sizeConfigVideo = { minViewPort: [0, 0], playerSize: [[640, 400]] }; + const videoActive = isSizeConfigActivated('video', sizeConfigVideo); + expect(videoActive).to.equal(true); + + // native test + const sizeConfigNative = { minViewPort: [0, 0], active: true }; + const nativeActive = isSizeConfigActivated('native', sizeConfigNative); + expect(nativeActive).to.equal(true); + }); + + it('should return "false" if mediaType does not match "banner", "video" or "native"', function () { + const sizeConfig = { minViewPort: [0, 0], sizes: [[300, 600], [970, 1200]] }; + const active = isSizeConfigActivated('unknownMediaType', sizeConfig); + expect(active).to.equal(false); + }); + }); + + describe('getActiveSizeBucket(sizeConfig, activeViewport)', function () { + it('should return the correct value of size bucket that is active (based on current viewport size) from a given set of size buckets defined in sizeConfig', function () { + const sizeConfig = [ + { minViewPort: [0, 0], sizes: [] }, + { minViewPort: [720, 500], sizes: [[300, 200], [300, 400]] }, + { minViewPort: [1200, 900], sizes: [[720, 400], [1000, 700]] } + ]; + + // test scenario 1 + const activeViewportA = [1000, 800]; + const activeSizeBucketA = getActiveSizeBucket(sizeConfig, activeViewportA); + expect(activeSizeBucketA).to.deep.equal([720, 500]); + + // test scenario 2 + const activeViewportB = [1300, 600]; + const activeSizeBucketB = getActiveSizeBucket(sizeConfig, activeViewportB); + expect(activeSizeBucketB).to.deep.equal([]); + }); + }); + + describe('getRelevantMediaTypesForBidder(sizeConfig, activeViewport)', function () { + beforeEach(function () { + sinon.spy(internal, 'checkBidderSizeConfigFormat'); + sinon.spy(internal, 'getActiveSizeBucket'); + }); + + afterEach(function () { + internal.checkBidderSizeConfigFormat.restore(); + internal.getActiveSizeBucket.restore(); + }); + it('should return an empty array if the bidder sizeConfig object is not formatted correctly', function () { + const sizeConfig = [ + { minViewPort: [], relevantMediaTypes: ['none'] }, + { minViewPort: [700, 0], relevantMediaTypes: ['banner', 'video'] } + ]; + const activeViewport = [720, 600]; + const relevantMediaTypes = getRelevantMediaTypesForBidder(sizeConfig, activeViewport); + expect(relevantMediaTypes).to.deep.equal([]); + }); + + it('should call function checkBidderSizeConfigFormat() once', function () { + const sizeConfig = [ + { minViewPort: [], relevantMediaTypes: ['none'] }, + { minViewPort: [700, 0], relevantMediaTypes: ['banner', 'video'] } + ]; + const activeViewport = [720, 600]; + getRelevantMediaTypesForBidder(sizeConfig, activeViewport); + + sinon.assert.callCount(internal.checkBidderSizeConfigFormat, 1); + sinon.assert.calledWith(internal.checkBidderSizeConfigFormat, sizeConfig); + }); + + it('should call function getActiveSizeBucket() once', function () { + const sizeConfig = [ + { minViewPort: [0, 0], relevantMediaTypes: ['none'] }, + { minViewPort: [700, 0], relevantMediaTypes: ['banner', 'video'] } + ]; + const activeViewport = [720, 600]; + getRelevantMediaTypesForBidder(sizeConfig, activeViewport); + + sinon.assert.callCount(internal.getActiveSizeBucket, 1); + sinon.assert.calledWith(internal.getActiveSizeBucket, sizeConfig, activeViewport); + }); + + it('should return the array contained in "relevantMediaTypes" property whose sizeBucket matches with the current viewport', function () { + const sizeConfig = [ + { minViewPort: [0, 0], relevantMediaTypes: ['none'] }, + { minViewPort: [700, 0], relevantMediaTypes: ['banner', 'video'] } + ]; + const activeVewport = [720, 600]; + const relevantMediaTypes = getRelevantMediaTypesForBidder(sizeConfig, activeVewport); + expect(relevantMediaTypes).to.deep.equal(['banner', 'video']); + }); + }); + + describe('getAdUnitDetail(auctionId, adUnit)', function () { + const adUnitDetailFixture_1 = { + adUnitCode: 'div-gpt-ad-1460505748561-0', + mediaTypes: {}, + sizeBucketToSizeMap: {}, + activeViewport: {}, + transformedMediaTypes: {} + }; + const adUnitDetailFixture_2 = { + adUnitCode: 'div-gpt-ad-1460505748561-1', + mediaTypes: { + banner: { + sizes: [[300, 250], [300, 600]] + }, + video: { + context: 'instream', + playerSize: [300, 460] + } + }, + sizeBucketToSizeMap: {}, + activeViewport: {}, + transformedMediaTypes: { banner: {}, video: {} } + } + beforeEach(function () { + sinon + .stub(sizeMappingInternalStore, 'getAuctionDetail') + .withArgs('a1b2c3') + .returns({ + usingSizeMappingV2: true, + adUnits: [adUnitDetailFixture_1] + }); + + sinon + .stub(sizeMappingInternalStore, 'setAuctionDetail') + .withArgs('a1b2c3', adUnitDetailFixture_2); + + sinon + .stub(internal, 'getFilteredMediaTypes') + .withArgs(adUnitDetailFixture_2.mediaTypes) + .returns(adUnitDetailFixture_2); + + sinon.spy(utils, 'logInfo'); + }); + + afterEach(function () { + sizeMappingInternalStore.getAuctionDetail.restore(); + sizeMappingInternalStore.setAuctionDetail.restore(); + internal.getFilteredMediaTypes.restore(); + utils.logInfo.restore(); + }); + + it('should return adUnit detail object from "sizeMappingInternalStore" if adUnit is alreay present in the store', function () { + const [adUnit] = utils.deepClone(AD_UNITS); + const adUnitDetail = getAdUnitDetail('a1b2c3', adUnit); + sinon.assert.callCount(sizeMappingInternalStore.getAuctionDetail, 1); + expect(adUnitDetail).to.deep.equal(adUnitDetailFixture_1); + }); + + it('should store value in "sizeMappingInterStore" object if adUnit is NOT preset in this object', function () { + const [, adUnit] = utils.deepClone(AD_UNITS); + const adUnitDetail = getAdUnitDetail('a1b2c3', adUnit); + sinon.assert.callCount(sizeMappingInternalStore.setAuctionDetail, 1); + sinon.assert.callCount(internal.getFilteredMediaTypes, 1); + expect(adUnitDetail).to.deep.equal(adUnitDetailFixture_2); + }); + + it('should log info message to show the details for activeSizeBucket', function () { + const [, adUnit] = utils.deepClone(AD_UNITS); + getAdUnitDetail('a1b2c3', adUnit); + sinon.assert.callCount(utils.logInfo, 1); + sinon.assert.calledWith(utils.logInfo, `SizeMappingV2:: AdUnit: div-gpt-ad-1460505748561-1 - Active size buckets after filtration: `, adUnitDetailFixture_2.sizeBucketToSizeMap); + }); + + it('should log info message if any of the mediaTypes defined in adUnit.mediaTypes got filtered out', function () { + const [, adUnit] = utils.deepClone(AD_UNITS); + + internal.getFilteredMediaTypes.restore(); + + const adUnitDetailFixture = { + adUnitCode: 'div-gpt-ad-1460505748561-1', + mediaTypes: { + banner: { + sizes: [[300, 250], [300, 600]] + }, + video: { + context: 'instream', + playerSize: [300, 460] + } + }, + sizeBucketToSizeMap: {}, + activeViewport: {}, + transformedMediaTypes: { banner: {} } + } + + sinon + .stub(internal, 'getFilteredMediaTypes') + .withArgs(adUnitDetailFixture.mediaTypes) + .returns(adUnitDetailFixture); + + getAdUnitDetail('a1b2c3', adUnit); + + sinon.assert.callCount(utils.logInfo, 2); + sinon.assert.calledWith(utils.logInfo.getCall(1), `SizeMappingV2:: AdUnit: div-gpt-ad-1460505748561-1 - mediaTypes that got filtered out: video`); + }); + }); + + describe('getFilteredMediaTypes(mediaTypes)', function () { + beforeEach(function () { + sinon + .stub(utils, 'getWindowTop') + .returns({ + innerWidth: 1680, + innerHeight: 269 + }); + + sinon.spy(utils, 'logWarn'); + }); + afterEach(function () { + utils.getWindowTop.restore(); + utils.logWarn.restore(); + }); + it('should return filteredMediaTypes object with all four properties (mediaTypes, transformedMediaTypes, activeViewport, sizeBucketToSizeMap) evaluated correctly', function () { + const [adUnit] = utils.deepClone(AD_UNITS); + const expectedMediaTypes = { + banner: { + sizeConfig: [ + { minViewPort: [0, 0], sizes: [] }, // remove if < 750px + { minViewPort: [750, 0], sizes: [[300, 250], [300, 600]] }, // between 750px and 1199px + { minViewPort: [1200, 0], sizes: [[970, 90], [728, 90], [300, 250]] }, // between 1200px and 1599px + { minViewPort: [1600, 0], sizes: [[1000, 300], [970, 90], [728, 90], [300, 250]] } // greater than 1600px + ] + }, + video: { + context: 'instream', + sizeConfig: [ + { minViewPort: [0, 0], playerSize: [] }, + { minViewPort: [800, 0], playerSize: [[640, 400]] }, + { minViewPort: [1200, 0], playerSize: [] } + ] + }, + native: { + image: { + required: true, + sizes: [150, 50] + }, + title: { + required: true, + len: 80 + }, + sponsoredBy: { + required: true + }, + clickUrl: { + required: true + }, + privacyLink: { + required: false + }, + body: { + required: true + }, + icon: { + required: true, + sizes: [50, 50] + }, + sizeConfig: [ + { minViewPort: [0, 0], active: false }, + { minViewPort: [600, 0], active: true }, + { minViewPort: [1000, 0], active: false } + ] + } + }; + const expectedSizeBucketToSizeMap = { + banner: { + activeSizeBucket: [1600, 0], + activeSizeDimensions: [[1000, 300], [970, 90], [728, 90], [300, 250]] + }, + video: { + activeSizeBucket: [1200, 0], + activeSizeDimensions: [] + }, + native: { + activeSizeBucket: [1000, 0], + activeSizeDimensions: 'NA' + } + }; + const expectedActiveViewport = [1680, 269]; + const expectedTransformedMediaTypes = { + banner: { + filteredSizeConfig: [ + { + minViewPort: [1600, 0], sizes: [[1000, 300], [970, 90], [728, 90], [300, 250]] + } + ], + sizeConfig: [ + { minViewPort: [0, 0], sizes: [] }, + { minViewPort: [750, 0], sizes: [[300, 250], [300, 600]] }, + { minViewPort: [1200, 0], sizes: [[970, 90], [728, 90], [300, 250]] }, + { minViewPort: [1600, 0], sizes: [[1000, 300], [970, 90], [728, 90], [300, 250]] } + ], + sizes: [[1000, 300], [970, 90], [728, 90], [300, 250] + ] + } + }; + const { mediaTypes, sizeBucketToSizeMap, activeViewport, transformedMediaTypes } = getFilteredMediaTypes(adUnit.mediaTypes); + expect(mediaTypes).to.deep.equal(expectedMediaTypes); + expect(activeViewport).to.deep.equal(expectedActiveViewport); + expect(sizeBucketToSizeMap).to.deep.equal(expectedSizeBucketToSizeMap); + expect(transformedMediaTypes).to.deep.equal(expectedTransformedMediaTypes); + }); + + it('should throw a warning message if Iframe blocks viewport size to be evaluated correctly', function () { + const [adUnit] = utils.deepClone(AD_UNITS); + utils.getWindowTop.restore(); + sinon + .stub(utils, 'getWindowTop') + .throws(); + getFilteredMediaTypes(adUnit.mediaTypes); + sinon.assert.callCount(utils.logWarn, 1); + sinon.assert.calledWith(utils.logWarn, `SizeMappingv2:: Unfriendly iframe blocks viewport size to be evaluated correctly`); + }); + }); + + describe('getBids({ bidderCode, auctionId, bidderRequestId, adUnits, labels, src })', function () { + const basic_AdUnit = [{ + code: 'adUnit1', + mediaTypes: { + banner: { + sizeConfig: [ + { minViewPort: [0, 0], sizes: [] }, + { minViewPort: [500, 0], sizes: [[300, 200], [400, 600]] } + ] + } + }, + bids: [{ + bidder: 'appnexus', + params: { + placementId: 13144370 + } + }, { + bidder: 'rubicon', + params: { + accountId: 14062, + siteId: 70608, + zoneId: 498816 + }, + sizeConfig: [ + { minViewPort: [0, 0], relevantMediaTypes: ['none'] }, + { minViewPort: [700, 0], relevantMediaTypes: ['banner'] } + ] + }], + transactionId: '123456' + }]; + const adUnitDetailFixture = { + adUnitCode: 'adUnit1', + transactionId: '123456', + sizes: [[300, 200], [400, 600]], + mediaTypes: { + banner: { + sizeConfig: [ + { minViewPort: [0, 0], sizes: [] }, + { minViewPort: [600, 0], sizes: [[300, 200], [400, 600]] } + ] + } + }, + sizeBucketToSizeMap: { + banner: { + activeSizeBucket: [[500, 0]], + activeSizeDimensions: [[300, 200], [400, 600]] + } + }, + activeViewport: [560, 260], + transformedMediaTypes: { + banner: { + filteredSizeConfig: [ + { minViewPort: [500, 0], sizes: [[300, 200], [400, 600]] } + ], + sizeConfig: [ + { minViewPort: [0, 0], sizes: [[]] }, + { minViewPort: [500, 0], sizes: [[300, 200], [400, 600]] } + ], + sizes: [[300, 200], [400, 600]] + } + } + }; + beforeEach(function () { + sinon + .stub(internal, 'getAdUnitDetail') + .withArgs('6d51e2d7-1447-4242-b6af-aaa5525a2c6e', basic_AdUnit[0]) + .returns(adUnitDetailFixture); + + sinon.spy(internal, 'getRelevantMediaTypesForBidder'); + + sinon.spy(utils, 'logInfo'); + sinon.spy(utils, 'logError'); + sinon.spy(utils, 'logWarn'); + }); + + afterEach(function () { + internal.getAdUnitDetail.restore(); + internal.getRelevantMediaTypesForBidder.restore(); + utils.logInfo.restore(); + utils.logError.restore(); + utils.logWarn.restore(); + }); + + it('should return an array of bids specific to the bidder', function () { + const expectedMediaTypes = { + banner: { + filteredSizeConfig: [ + { minViewPort: [500, 0], sizes: [[300, 200], [400, 600]] } + ], + sizeConfig: [ + { minViewPort: [0, 0], sizes: [[]] }, + { minViewPort: [500, 0], sizes: [[300, 200], [400, 600]] } + ], + sizes: [[300, 200], [400, 600]] + } + }; + + const bidRequests_1 = getBids({ + bidderCode: 'appnexus', + auctionId: '6d51e2d7-1447-4242-b6af-aaa5525a2c6e', + bidderRequestId: '393a43193a0ac', + adUnits: basic_AdUnit, + labels: [], + src: 'client' + }); + expect(bidRequests_1[0].mediaTypes).to.deep.equal(expectedMediaTypes); + expect(bidRequests_1[0].bidder).to.equal('appnexus'); + + const bidRequests_2 = getBids({ + bidderCode: 'rubicon', + auctionId: '6d51e2d7-1447-4242-b6af-aaa5525a2c6e', + bidderRequestId: '393a43193a0aa', + adUnits: basic_AdUnit, + labels: [], + src: 'client' + }); + expect(bidRequests_2[0]).to.be.undefined; + sinon.assert.callCount(internal.getRelevantMediaTypesForBidder, 1); + }); + + it('should log an error message if ad unit is disabled because there are no active media types left after size config filtration', function () { + internal.getAdUnitDetail.restore(); + + const adUnit = utils.deepClone(basic_AdUnit); + adUnit[0].mediaTypes.banner.sizeConfig = [ + { minViewPort: [0, 0], sizes: [] }, + { minViewPort: [600, 0], sizes: [[300, 200], [400, 600]] } + ]; + + const adUnitDetailFixture = { + adUnitCode: 'adUnit1', + mediaTypes: { + banner: { + sizeConfig: [ + { minViewPort: [0, 0], sizes: [] }, + { minViewPort: [600, 0], sizes: [[300, 200], [400, 600]] } + ] + } + }, + sizeBucketToSizeMap: { + banner: { + activeSizeBucket: [0, 0], + activeSizeDimensions: [[]] + } + }, + activeViewport: [560, 260], + transformedMediaTypes: {} + }; + + sinon + .stub(internal, 'getAdUnitDetail') + .withArgs('6d51e2d7-1447-4242-b6af-aaa5525a2c6e', adUnit[0]) + .returns(adUnitDetailFixture); + + const bidRequests = getBids({ + bidderCode: 'appnexus', + auctionId: '6d51e2d7-1447-4242-b6af-aaa5525a2c6e', + bidderRequestId: '393a43193a0ac', + adUnits: adUnit, + labels: [], + src: 'client' + }); + expect(bidRequests[0]).to.be.undefined; + sinon.assert.callCount(utils.logInfo, 1); + sinon.assert.calledWith(utils.logInfo, `SizeMappingV2:: Ad Unit: adUnit1 is disabled since there are no active media types after sizeConfig filtration.`); + }); + + it('should throw an error if bidder level sizeConfig is not configured properly', function () { + internal.getAdUnitDetail.restore(); + + const adUnit = utils.deepClone(basic_AdUnit); + adUnit[0].bids[1].sizeConfig = [ + { minViewPort: [], relevantMediaTypes: ['none'] }, + { minViewPort: [700, 0], relevantMediaTypes: ['banner'] } + ]; + + sinon + .stub(internal, 'getAdUnitDetail') + .withArgs('6d51e2d7-1447-4242-b6af-aaa5525a2c6e', adUnit[0]) + .returns(adUnitDetailFixture); + + const bidRequests = getBids({ + bidderCode: 'rubicon', + auctionId: '6d51e2d7-1447-4242-b6af-aaa5525a2c6e', + bidderRequestId: '393a43193a0ac', + adUnits: adUnit, + labels: [], + src: 'client' + }); + + expect(bidRequests[0]).to.not.be.undefined; + sinon.assert.callCount(utils.logError, 1); + sinon.assert.calledWith(utils.logError, `SizeMappingV2:: AdUnit: adUnit1, Bidder: rubicon - sizeConfig is not configured properly. This bidder won't be eligible for sizeConfig checks and will remail active.`); + }); + + it('should ensure bidder relevantMediaTypes is a subset of active media types at the ad unit level', function () { + internal.getAdUnitDetail.restore(); + + const adUnit = utils.deepClone(basic_AdUnit); + adUnit[0].bids[1].sizeConfig = [ + { minViewPort: [0, 0], relevantMediaTypes: ['none'] }, + { minViewPort: [400, 0], relevantMediaTypes: ['banner'] } + ]; + + sinon + .stub(internal, 'getAdUnitDetail') + .withArgs('6d51e2d7-1447-4242-b6af-aaa5525a2c6e', adUnit[0]) + .returns(adUnitDetailFixture); + + const bidRequests = getBids({ + bidderCode: 'rubicon', + auctionId: '6d51e2d7-1447-4242-b6af-aaa5525a2c6e', + bidderRequestId: '393a43193a0ac', + adUnits: adUnit, + labels: [], + src: 'client' + }); + expect(bidRequests[0]).to.not.be.undefined; + expect(bidRequests[0].mediaTypes.banner).to.not.be.undefined; + expect(bidRequests[0].mediaTypes.banner.sizes).to.deep.equal([[300, 200], [400, 600]]); + }); + + it('should logInfo if bidder relevantMediaTypes contains media type that is not active at the ad unit level', function () { + internal.getAdUnitDetail.restore(); + + const adUnit = utils.deepClone(basic_AdUnit); + adUnit[0].mediaTypes = { + banner: { + sizeConfig: [ + { minViewPort: [0, 0], sizes: [] }, + { minViewPort: [700, 0], sizes: [[300, 200], [400, 600]] } + ] + }, + native: { + sizeConfig: [ + { minViewPort: [0, 0], active: false }, + { minViewPort: [400, 0], active: true } + ] + } + }; + + adUnit[0].bids[1].sizeConfig = [ + { minViewPort: [0, 0], relevantMediaTypes: ['none'] }, + { minViewPort: [200, 0], relevantMediaTypes: ['banner'] } + ]; + + const adUnitDetailFixture = { + adUnitCode: 'adUnit1', + mediaTypes: { + banner: { + sizeConfig: [ + { minViewPort: [0, 0], sizes: [] }, + { minViewPort: [700, 0], sizes: [[300, 200], [400, 600]] } + ] + }, + native: { + sizeConfig: [ + { minViewPort: [0, 0], active: false }, + { minViewPort: [400, 0], active: true } + ] + } + }, + sizeBucketToSizeMap: { + banner: { + activeSizeBucket: [0, 0], + activeSizeDimensions: [[]] + }, + native: { + activeSizeBucket: [400, 0], + activeSizeDimensions: 'NA' + } + }, + activeViewport: [560, 260], + transformedMediaTypes: { + native: {} + } + }; + + sinon + .stub(internal, 'getAdUnitDetail') + .withArgs('6d51e2d7-1447-4242-b6af-aaa5525a2c6e', adUnit[0]) + .returns(adUnitDetailFixture); + + const bidRequests = getBids({ + bidderCode: 'rubicon', + auctionId: '6d51e2d7-1447-4242-b6af-aaa5525a2c6e', + bidderRequestId: '393a43193a0ac', + adUnits: adUnit, + labels: [], + src: 'client' + }); + expect(bidRequests[0]).to.be.undefined; + sinon.assert.callCount(utils.logInfo, 1); + sinon.assert.calledWith(utils.logInfo, `SizeMappingV2:: AdUnit: adUnit1, Bidder: rubicon - 'relevantMediaTypes' for this bidder does not match with any of the active mediaTypes at the Ad Unit level. This bidder is disabled.`); + }); + + it('should throw a warning if mediaTypes object is not correctly formatted', function () { + sinon + .stub(utils, 'isValidMediaTypes') + .returns(false); + + getBids({ + bidderCode: 'appnexus', + auctionId: '6d51e2d7-1447-4242-b6af-aaa5525a2c6e', + bidderRequestId: '393a43193a0ac', + adUnits: basic_AdUnit, + labels: [], + src: 'client' + }); + sinon.assert.callCount(utils.logWarn, 1); + sinon.assert.calledWith(utils.logWarn, `SizeMappingV2:: Ad Unit: adUnit1 has declared invalid mediaTypes or has not declared a mediaTypes property`); + + utils.isValidMediaTypes.restore(); + }); + + it('should log a message if ad unit is disabled due to a failing label check', function () { + sinon + .stub(internal, 'isLabelActivated') + .onFirstCall() + .returns(false); + + getBids({ + bidderCode: 'appnexus', + auctionId: '6d51e2d7-1447-4242-b6af-aaa5525a2c6e', + bidderRequestId: '393a43193a0ac', + adUnits: basic_AdUnit, + labels: [], + src: 'client' + }); + + sinon.assert.callCount(utils.logInfo, 1); + sinon.assert.calledWith(utils.logInfo, `SizeMappingV2:: Ad Unit: adUnit1 is disabled due to failing label check.`); + + internal.isLabelActivated.restore(); + }); + + it('should log a message if bidder is disabled due to a failing label check', function () { + const stub = sinon.stub(internal, 'isLabelActivated') + stub.onFirstCall().returns(true); + stub.onSecondCall().returns(false); + + getBids({ + bidderCode: 'appnexus', + auctionId: '6d51e2d7-1447-4242-b6af-aaa5525a2c6e', + bidderRequestId: '393a43193a0ac', + adUnits: basic_AdUnit, + labels: [], + src: 'client' + }); + + sinon.assert.callCount(utils.logInfo, 1); + sinon.assert.calledWith(utils.logInfo, `SizeMappingV2:: AdUnit: adUnit1, Bidder: appnexus - Label check for this bidder has failed. This bidder is disabled.`); + + internal.isLabelActivated.restore(); + }) + }); +});