diff --git a/README.md b/README.md index f230f60..1feae04 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ To get started you must include the script tag for ArcAds in your page header, l ``` -Additionally you can install the package with npm. This is mostly useful for when you're integrating ArcAds into a single page application or a JavaScript heavy project. Most implementations should just include the script in the page header. +Additionally, you can install the package with npm. This is mostly useful for when you're integrating ArcAds into a single page application or a JavaScript heavy project. Most implementations should just include the script in the page header. ``` npm install arcads @@ -58,17 +58,17 @@ The following table shows all of the possible parameters the `registerAd` method | `slotName` | The `slotName` parameter is equal to the slot name configured within DFP, for example `sitename/hp/hp-1`. The publisher ID gets attached to the slot name within the ArcAds logic. | `String` | `Required` | | `dimensions` | The `dimensions` parameter should be an array with array of arrays containing the advertisement sizes the slot can load. If left empty the advertisement will be considered as an out of page unit. | `Array` | `Optional` | | `adType` | The `adType` parameter should describe the type of advertisement, for instance `leaderboard` or `cube`. | `String` | `Optional` | -| `display` | The `display` paramter determines which user agents can render the advertisement. The available choices are `desktop`, `mobile`, or `all`. If a value is not provided it will default to `all`. | `String` | `Optional` | -| `targeting` | The `targeting` paramter accepts an object containing key/value pairs which should attached to the advertisement request. | `Object` | `Optional` | -| `sizemap` | The `sizemap` paramter accepts an object containing information about the advertisements size mapping, for more information refer to the [Size Mapping portion of the readme](https://github.com/washingtonpost/arcads#size-mapping). | `Object` | `Optional` | -| `bidding` | The `bidding` paramter accepts an object containing information about the advertisements header bidding vendors, for more information refer to the [Header Bidding portion of the readme](https://github.com/washingtonpost/arcads#header-bidding). | `Object` | `Optional` | +| `display` | The `display` parameter determines which user agents can render the advertisement. The available choices are `desktop`, `mobile`, or `all`. If a value is not provided it will default to `all`. | `String` | `Optional` | +| `targeting` | The `targeting` parameter accepts an object containing key/value pairs which should attached to the advertisement request. | `Object` | `Optional` | +| `sizemap` | The `sizemap` parameter accepts an object containing information about the advertisements size mapping, for more information refer to the [Size Mapping portion of the readme](https://github.com/washingtonpost/arcads#size-mapping). | `Object` | `Optional` | +| `bidding` | The `bidding` parameter accepts an object containing information about the advertisements header bidding vendors, for more information refer to the [Header Bidding portion of the readme](https://github.com/washingtonpost/arcads#header-bidding). | `Object` | `Optional` | | `prerender` | The `prerender` parameter accepts an a function that should fire before the advertisement loads, for more information refer to the [Prerender Hook portion of the readme](https://github.com/washingtonpost/arcads/tree/master#prerender-hook). | `Function` | `Optional` | ### Out of Page Ads If an advertisement has an empty or missing `dimensions` parameter it will be considered as a [DFP Out of Page creative](https://support.google.com/dfp_premium/answer/6088046?hl=en) and rendered as such. ### Callback -Whenever an advertisement loads you can access data about the advertisement such as its size and id by passing in an optional callback to the initialization of ArcAds. This ties a handler to the `slotRenderEnded` event that DFP emits and is called everytime an advertisement is about to render, allowing you to make any page layout modifications to accomodate a specific advertisement. +Whenever an advertisement loads you can access data about the advertisement such as its size and id by passing in an optional callback to the initialization of ArcAds. This ties a handler to the `slotRenderEnded` event that DFP emits and is called every time an advertisement is about to render, allowing you to make any page layout modifications to accommodate a specific advertisement. ```javascript const arcAds = new ArcAds({ @@ -115,9 +115,9 @@ arcAds.registerAd({ }) ``` -The service will automatically give the advertisement a `position` target key/value pair if either the `targeting` object or `position` key of the targeting object are not present. The position value will incriment by 1 in sequence for each of the same `adType` on the page. This is a common practice between ad traffickers so this behavior is baked in, only if the trafficker makes use of this targeting will it have any effect on the advertisement rendering. +The service will automatically give the advertisement a `position` target key/value pair if either the `targeting` object or `position` key of the targeting object are not present. The position value will increment by 1 in sequence for each of the same `adType` on the page. This is a common practice between ad traffickers so this behavior is baked in, only if the trafficker makes use of this targeting will it have any effect on the advertisement rendering. -If `adType` is exluded from the `registerAd` call the automatic position targeting will not be included. +If `adType` is excluded from the `registerAd` call the automatic position targeting will not be included. ## Size Mapping You can configure DFP size mapped ads with the same registration call by adding a `sizemap` object. To utilize size mapping the `dimensions` key should be updated to include an array representing a nested array of arrays containing the applicable sizes for a specific breakpoint. @@ -157,7 +157,7 @@ arcAds.registerAd({ ## Prerender Hook ArcAds provides a way for you to get information about an advertisement before it loads, which is useful for attaching targeting data from third party vendors. -You can setup a function within the `registerAd` call by adding a `prerender` paramter, the value of which being the function you'd like to fire before the advertisement loads. This function will also fire before the advertisement refreshes if you're using sizemapping. +You can setup a function within the `registerAd` call by adding a `prerender` parameter, the value of which being the function you'd like to fire before the advertisement loads. This function will also fire before the advertisement refreshes if you're using sizemapping. ```javascript arcAds.registerAd({ @@ -217,7 +217,7 @@ If you'd like to include Prebid.js you must include the library before `arcads.j ``` -You can enable Prebid.js on the wrapper by adding a `prebid` object to the wrapper initialization and setting `enabled: true`. You can also optionally pass it a `timeout` value which corresponds in milliseconds how long Prebid.js will wait until it closs out the bidding for the advertisements on the page. By default the timeout will be set to `700`. +You can enable Prebid.js on the wrapper by adding a `prebid` object to the wrapper initialization and setting `enabled: true`. You can also optionally pass it a `timeout` value which corresponds in milliseconds how long Prebid.js will wait until it closes out the bidding for the advertisements on the page. By default, the timeout will be set to `700`. ```javascript const arcAds = new ArcAds({ @@ -292,7 +292,7 @@ const arcAds = new ArcAds({ }) ``` -On the advertisement registration you can then provide information about which bidding services that specific advertisement should use. You can find a list of paramters that Prebid.js accepts for each adapter on the [Prebid.js website](http://prebid.org/dev-docs/publisher-api-reference.html). Additionally you can turn on [Prebid.js debugging](http://prebid.org/dev-docs/toubleshooting-tips.html) by adding `?pbjs_debug=true` to the url. +On the advertisement registration you can then provide information about which bidding services that specific advertisement should use. You can find a list of parameters that Prebid.js accepts for each adapter on the [Prebid.js website](http://prebid.org/dev-docs/publisher-api-reference.html). Additionally you can turn on [Prebid.js debugging](http://prebid.org/dev-docs/toubleshooting-tips.html) by adding `?pbjs_debug=true` to the url. ```javascript arcAds.registerAd({ @@ -363,8 +363,10 @@ arcAds.registerAd({ }) ``` +NOTE: Currently Amazon A9/TAM is not supported for use with Singe Request Architecture (SRA). + ## Registering Multiple Ads -You can display multiple ads at once using the `registerAdCollection` method. This is useful if you're initializing multiple advertisements at once in the page header. To do this you can pass an array of advertisement objects similar to the one you would with the `registerAd` call. +You can display multiple ads at once using the `registerAdCollection` method. This is useful if you're initializing multiple advertisements at once in the page header. To do this you can pass an array of advertisement objects similar to the one you would with the `registerAd` call. Note that when using this function, if setAdsBlockGate() has not been called, the calls for each ad will be made individually. If you need to achieve Single Request Architecture, see the documentation below, "SRA Single Request Architecture". ```javascript const ads = [{ @@ -413,8 +415,23 @@ const ads = [{ arcAds.registerAdCollection(ads) ``` +## SRA Single Request Architecture +SRA architecture Functions will allow all ads to go out in one single ad call. The functions are presented in the order they should be called: + +1. setPageLevelTargeting(key, value): sets targeting parameters- applied to all ads on the page. Extracting common targeting values is recommended in order to avoid repeating targeting for each ad in the single ad call. +1. setAdsBlockGate(): “closes” the gate - as ads are added, calls do not go out. This allows ads configurations to accumulated to be set out later, together all at once. +1. reserveAd(params): accumulates ads to be sent out later. This function is called once per one ad. +1. releaseAdsBlockGate(): “opens” the gate - allows an ad call to go out. +1. sendSingleCallAds(): registers all the ads added via reserveAd(), and sends out a single ad call (SRA call) containing all the ads information that has been added so far via reserveAd(). + +To add more ads, repeat steps 1-5 as needed. + +NOTE: Prebid is supported for SRA. Amazon A9/TAM is not supported for SRA and will need to be implemented at a future date. + +NOTE: ArcAds SRA implementation calls enableSingleRequest() which means that when using pubads lazyLoad functions together with SRA, when the first ad slot comes within the viewport specified by the fetchMarginPercent parameter, the call for that ad and all other ad slots is made. If different behavior is desired after the initial SRA call is made, an outside lazy loading library may be used to manage the calls for regsterAd, reserveAd and other calls. + ## Developer Tools -There's a series developer tools availble, to get started run `yarn install`. +There's a series developer tools available, to get started run `yarn install`. | Command | Description | | ------------- | ------------- | @@ -425,9 +442,10 @@ There's a series developer tools availble, to get started run `yarn install`. | `yarn debug` | Starts a local http server so you can link directly to the script during development. For example ` | ### Slot Override -You can override the slot name of every advertisement on the page by appending `?adslot=` to the URL. This will override whatever is placed inside of the `slotName` field when invoking the `registerAd` method. For example if you hit the URL `arcpublishing.com/?adslot=homepage/myad`, the full ad slot path will end up being your DFP id followed by the value: `123/homepage/myad`. +You can override the slot name of every advertisement on the page by appending `?adslot=` to the URL. This will override whatever is placed inside of the `slotName` field when invoking the `registerAd` method. For example, if you hit the URL `arcpublishing.com/?adslot=homepage/myad`, the full ad slot path will end up being your DFP id followed by the value: `123/homepage/myad`. You can also debug slot names and GPT in general by typing `window.googletag.openConsole()` into the browsers developer console. + ## Contributing If you'd like to contribute to ArcAds please read our [contributing guide](https://github.com/washingtonpost/ArcAds/blob/master/CONTRIBUTING.md). diff --git a/dist/arcads.js b/dist/arcads.js index cd73372..ac706bc 100644 --- a/dist/arcads.js +++ b/dist/arcads.js @@ -1 +1 @@ -!function(e,n){if("object"==typeof exports&&"object"==typeof module)module.exports=n();else if("function"==typeof define&&define.amd)define([],n);else{var i=n();for(var t in i)("object"==typeof exports?exports:e)[t]=i[t]}}("undefined"!=typeof self?self:this,function(){return function(e){var n={};function i(t){if(n[t])return n[t].exports;var r=n[t]={i:t,l:!1,exports:{}};return e[t].call(r.exports,r,r.exports,i),r.l=!0,r.exports}return i.m=e,i.c=n,i.d=function(e,n,t){i.o(e,n)||Object.defineProperty(e,n,{configurable:!1,enumerable:!0,get:t})},i.n=function(e){var n=e&&e.__esModule?function(){return e.default}:function(){return e};return i.d(n,"a",n),n},i.o=function(e,n){return Object.prototype.hasOwnProperty.call(e,n)},i.p="",i(i.s=3)}([function(e,n,i){"use strict";Object.defineProperty(n,"__esModule",{value:!0}),n.initializeGPT=function(){window.googletag=window.googletag||{},window.googletag.cmd=window.googletag.cmd||[],(0,t.appendResource)("script","//www.googletagservices.com/tag/js/gpt.js",!0,!0)},n.refreshSlot=function(e){var n=e.ad,i=e.correlator,t=void 0!==i&&i,r=e.prerender,o=void 0===r?null:r,a=e.info,d=void 0===a?{}:a;new Promise(function(e){if(o)try{o(d).then(function(){e("Prerender function has completed.")})}catch(n){console.warn("ArcAds: Prerender function did not return a promise or there was an error.\n Documentation: https://github.com/wapopartners/arc-ads/wiki/Utilizing-a-Prerender-Hook"),e("Prerender function did not return a promise or there was an error, ignoring.")}else e("No Prerender function was provided.")}).then(function(){!function e(){if(window.blockArcAdsLoad)return;window.googletag&&googletag.pubadsReady?window.googletag.pubads().refresh([n],{changeCorrelator:t}):setTimeout(function(){e()},200)}()})},n.queueGoogletagCommand=function(e){window.googletag.cmd.push(e)},n.setTargeting=function(e,n){for(var i in n)n.hasOwnProperty(i)&&n[i]&&e.setTargeting(i,n[i])},n.dfpSettings=function(e){window.googletag.pubads().disableInitialLoad(),window.googletag.pubads().enableSingleRequest(),window.googletag.pubads().enableAsyncRendering(),this.collapseEmptyDivs&&window.googletag.pubads().collapseEmptyDivs();window.googletag.enableServices(),e&&window.googletag.pubads().addEventListener("slotRenderEnded",e)},n.determineSlotName=function(e,n){var i=(0,r.expandQueryString)("adslot");if(i&&(""!==i||null!==i))return"/"+e+"/"+i;return"/"+e+"/"+n};var t=i(5),r=i(6)},function(e,n,i){"use strict";Object.defineProperty(n,"__esModule",{value:!0}),n.initializeBiddingServices=a,n.fetchBids=function(e){var n=this,i=e.ad,d=e.id,s=e.slotName,u=e.dimensions,l=e.wrapper,c=e.bidding,p=e.correlator,f=void 0!==p&&p,g=e.prerender,h=e.breakpoints,b={adUnit:i,adSlot:s,adDimensions:u,adId:d,bids:c},v=new Promise(function(e){if(l.prebid&&l.prebid.enabled){var r=l.prebid.timeout||700;t.queuePrebidCommand.bind(n,(0,t.fetchPrebidBids)(i,l.prebid.useSlotForAdUnit?s:d,r,b,g,function(){e("Fetched Prebid ads!")}))}else e("Prebid is not enabled on the wrapper...")}),m=new Promise(function(e){l.amazon&&l.amazon.enabled?(0,r.fetchAmazonBids)(d,s,u,h,function(){e("Fetched Amazon ads!")}):e("Amazon is not enabled on the wrapper...")});window.arcBiddingReady?Promise.all([v,m]).then(function(){(0,o.refreshSlot)({ad:i,correlator:f,prerender:g,info:b})}):setTimeout(function(){return a()},200)};var t=i(2),r=i(7),o=i(0);function a(e){var n=e.prebid,i=void 0!==n&&n,t=e.amazon,o=void 0!==t&&t;if(!window.arcBiddingReady){window.arcBiddingReady=!1;var a=new Promise(function(e){if(i&&i.enabled){if("undefined"==typeof pbjs){var n=n||{};n.que=n.que||[]}e("Prebid has been initialized")}else e("Prebid is not enabled on the wrapper...")}),d=new Promise(function(e){o&&o.enabled&&window.apstag?o.id&&""!==o.id?(0,r.queueAmazonCommand)(function(){window.apstag.init({pubID:o.id,adServer:"googletag"}),e("Amazon scripts have been added onto the page!")}):(console.warn("ArcAds: Missing Amazon account id. \n Documentation: https://github.com/wapopartners/arc-ads#amazon-tama9"),e("Amazon is not enabled on the wrapper...")):e("Amazon is not enabled on the wrapper...")});Promise.all([a,d]).then(function(){window.arcBiddingReady=!0})}}},function(e,n,i){"use strict";Object.defineProperty(n,"__esModule",{value:!0});var t=Object.assign||function(e){for(var n=1;n5&&void 0!==arguments[5]?arguments[5]:null;pbjs.addAdUnits(t),pbjs.requestBids({timeout:i,adUnitCodes:[n],bidsBackHandler:function(i){console.log("Bid Back Handler",i),pbjs.setTargetingForGPTAsync([n]),a?a():(0,r.refreshSlot)({ad:e,info:t,prerender:o})}})},n.addUnit=function(e,n,i){var r=arguments.length>3&&void 0!==arguments[3]?arguments[3]:{},o=arguments.length>4&&void 0!==arguments[4]?arguments[4]:{},a=t({code:e,bids:i},o);a.mediaTypes={banner:{sizes:n}};var d=r.sizeConfig,s=r.config;if(pbjs.addAdUnits(a),s)return void pbjs.setConfig(s);d&&pbjs.setConfig({sizeConfig:d})};var r=i(0)},function(e,n,i){"use strict";Object.defineProperty(n,"__esModule",{value:!0}),n.ArcAds=void 0;var t=function(){function e(e,n){for(var i=0;i1&&void 0!==arguments[1]?arguments[1]:null;!function(e,n){if(!(e instanceof n))throw new TypeError("Cannot call a class as a function")}(this,e),this.dfpId=n.dfp.id||"",this.wrapper=n.bidding||{},this.positions=[],this.collapseEmptyDivs=n.dfp.collapseEmptyDivs,window.isMobile=r.MobileDetection,""===this.dfpId?console.warn("ArcAds: DFP id is missing from the arcads initialization script. \n Documentation: https://github.com/wapopartners/arc-ads#getting-started"):((0,a.initializeGPT)(),(0,a.queueGoogletagCommand)(a.dfpSettings.bind(this,i)),(0,o.initializeBiddingServices)(this.wrapper))}return t(e,[{key:"registerAd",value:function(e){var n=e.id,i=e.slotName,t=e.dimensions,r=e.adType,o=void 0!==r&&r,s=e.targeting,l=void 0===s?{}:s,c=e.display,p=void 0===c?"all":c,f=e.bidding,g=void 0!==f&&f,h=e.iframeBidders,b=void 0===h?["openx"]:h,v=e.others,m=void 0===v?{}:v,w=[],y=!1,A=function e(n){return Array.isArray(n)?1+Math.max.apply(Math,u(n.map(function(n){return e(n)}))):0}(t);t&&void 0!==t&&1===A?w.push.apply(w,u(t)):t&&void 0!==t&&t.length>0&&2===A?w.push.apply(w,u(t)):t&&t.forEach(function(e){w.push.apply(w,u(e))});try{if(!(l&&l.hasOwnProperty("position")||!1===o)){var P=this.positions[o]+1||1;this.positions[o]=P;var k=Object.assign(l,{position:P});Object.assign(e,{targeting:k})}if(isMobile.any()&&"mobile"===p||!isMobile.any()&&"desktop"===p||"all"===p){if(g.prebid&&g.prebid.bids&&this.wrapper.prebid&&this.wrapper.prebid.enabled&&w){pbjs&&b.length>0&&pbjs.setConfig({userSync:{iframeEnabled:!0,filterSettings:{iframe:{bidders:b,filter:"include"}}}});var z=this.wrapper.prebid.useSlotForAdUnit?(0,a.determineSlotName)(this.dfpId,i):n;d.queuePrebidCommand.bind(this,(0,d.addUnit)(z,w,g.prebid.bids,this.wrapper.prebid,m))}(y=this.displayAd.bind(this,e))&&(0,a.queueGoogletagCommand)(y)}}catch(e){console.error("ads error",e)}}},{key:"registerAdCollection",value:function(e){var n=this;e.forEach(function(e){n.registerAd(e)})}},{key:"displayAd",value:function(e){var n=e.id,i=e.slotName,t=e.dimensions,r=e.targeting,d=e.sizemap,u=void 0!==d&&d,l=e.bidding,c=void 0!==l&&l,p=e.prerender,f=void 0===p?null:p,g=(0,a.determineSlotName)(this.dfpId,i),h=t&&!t.length?null:t,b=t?window.googletag.defineSlot(g,h,n):window.googletag.defineOutOfPageSlot(g,n);if(u&&u.breakpoints&&t){var v=(0,s.prepareSizeMaps)(h,u.breakpoints),m=v.mapping,w=v.breakpoints,y=v.correlators;if(!b)return!1;b.defineSizeMapping(m),u.refresh&&(0,s.setResizeListener)({ad:b,slotName:g,breakpoints:w,id:n,mapping:m,correlators:y,bidding:c,wrapper:this.wrapper,prerender:f})}b&&(b.addService(window.googletag.pubads()),(0,a.setTargeting)(b,r));var A=u&&u.breakpoints?u.breakpoints:[];t&&c&&(c.amazon&&c.amazon.enabled||c.prebid&&c.prebid.enabled)?(0,o.fetchBids)({ad:b,id:n,slotName:g,dimensions:h,wrapper:this.wrapper,prerender:f,bidding:c,breakpoints:A}):(0,a.refreshSlot)({ad:b,prerender:f,info:{adUnit:b,adSlot:g,adDimensions:h,adId:n}})}}]),e}()},function(e,n,i){"use strict";Object.defineProperty(n,"__esModule",{value:!0});var t=function(){function e(e,n){for(var i=0;i1}},{key:"any",value:function(){return this.Android()||this.Kindle()||this.KindleFire()||this.Silk()||this.BlackBerry()||this.iOS()||this.Windows()||this.FirefoxOS()}}]),e}();n.default=r},function(e,n,i){"use strict";Object.defineProperty(n,"__esModule",{value:!0}),n.appendResource=function(e,n,i,t,r){var o=document.createElement(e);if("script"!==e)return;o.src=n,o.async=i||!1,o.defer=i||t||!1;(document.head||document.documentElement).appendChild(o),r&&r()}},function(e,n,i){"use strict";Object.defineProperty(n,"__esModule",{value:!0}),n.expandQueryString=function(e){var n=window.location.href,i=e.replace(/[[\]]/g,"\\$&"),t=new RegExp("[?&]"+i+"(=([^&#]*)|&|#|$)").exec(n);if(!t)return null;if(!t[2])return"";return decodeURIComponent(t[2].replace(/\+/g," "))}},function(e,n,i){"use strict";function t(e){window.apstag&&e()}Object.defineProperty(n,"__esModule",{value:!0}),n.fetchAmazonBids=function(e,n,i,r){var o=arguments.length>4&&void 0!==arguments[4]?arguments[4]:null,a=i;if(r&&void 0!==window.innerWidth&&void 0!==i[0][0][0]){for(var d=window.innerWidth,s=-1,u=r.length,l=0;l=r[l][0]){s=l;break}a=i[s]}t(function(){var i={slotName:n,slotID:e,sizes:a};window.apstag.fetchBids({slots:[i]},function(){window.apstag.setDisplayBids(),o&&o()})})},n.queueAmazonCommand=t},function(e,n,i){"use strict";Object.defineProperty(n,"__esModule",{value:!0}),n.resizeListeners=n.sizemapListeners=void 0,n.prepareSizeMaps=function(e,n){var i=[],t=[],r=[];return(n.length?n:null).forEach(function(n,o){i.push([n,e[o]]),-1===t.indexOf(n[0])&&(t.push(n[0]),r.push(!1))}),t.sort(function(e,n){return e-n}),{mapping:i,breakpoints:t,correlators:r}},n.parseSizeMappings=s,n.runResizeEvents=u,n.setResizeListener=function(e){var n=e.id,i=e.correlators;d[n]=(0,t.debounce)(u(e),250),window.addEventListener("resize",d[n]),a[n]={listener:d[n],correlators:i}};var t=i(9),r=i(1),o=i(0),a=n.sizemapListeners={},d=n.resizeListeners={};function s(e){try{var n=[window.innerWidth||document.documentElement.clientWidth||document.body.clientWidth,window.innerHeight||document.documentElement.clientHeight||document.body.clientHeight],i=e.filter(function(e){return e[0][0]<=n[0]&&e[0][1]<=n[1]}),t=i.length>0?i[0][1]:[];return t.length>0&&t[0].constructor!==Array&&(t=[t]),t}catch(n){return e[e.length-1][1]}}function u(e){var n=void 0,i=!1;return function(){for(var t=e.ad,d=e.breakpoints,u=e.id,l=e.bidding,c=e.mapping,p=e.slotName,f=e.wrapper,g=e.prerender,h=window.innerWidth,b=void 0,v=void 0,m=0;mb&&(h5&&void 0!==arguments[5]?arguments[5]:null,d=r;d.bids=Array.isArray(r.bids)?r.bids:[r.bids],o(e,[n],i,d,t,a)},n.addUnit=function(e,n,i){var t=arguments.length>3&&void 0!==arguments[3]?arguments[3]:{},o=arguments.length>4&&void 0!==arguments[4]?arguments[4]:{},a=r({code:e,bids:i},o);a.mediaTypes={banner:{sizes:n}};var d=t.sizeConfig,s=t.config;if(pbjs.addAdUnits(a),s)return void pbjs.setConfig(s);d&&pbjs.setConfig({sizeConfig:d})};var t=i(0);function o(e,n,i,r,o){var a=arguments.length>5&&void 0!==arguments[5]?arguments[5]:null;pbjs.addAdUnits(r),window.blockArcAdsPrebid||pbjs.requestBids({timeout:i,adUnitCodes:n,bidsBackHandler:function(n){console.log("Bid Back Handler",n),pbjs.setTargetingForGPTAsync([code]),a?a():(0,t.refreshSlot)({ad:e,info:r,prerender:o})}})}},function(e,n,i){"use strict";Object.defineProperty(n,"__esModule",{value:!0}),n.ArcAds=void 0;var r=function(){function e(e,n){for(var i=0;i1&&void 0!==arguments[1]?arguments[1]:null;!function(e,n){if(!(e instanceof n))throw new TypeError("Cannot call a class as a function")}(this,e),this.dfpId=n.dfp.id||"",this.wrapper=n.bidding||{},this.positions=[],this.collapseEmptyDivs=n.dfp.collapseEmptyDivs,window.isMobile=t.MobileDetection,""===this.dfpId?console.warn("ArcAds: DFP id is missing from the arcads initialization script. \n Documentation: https://github.com/wapopartners/arc-ads#getting-started"):((0,a.initializeGPT)(),(0,a.queueGoogletagCommand)(a.dfpSettings.bind(this,i)),(0,o.initializeBiddingServices)(this.wrapper))}return r(e,[{key:"registerAd",value:function(e){var n=e.id,i=e.slotName,r=e.dimensions,t=e.adType,o=void 0!==t&&t,s=e.targeting,c=void 0===s?{}:s,l=e.display,p=void 0===l?"all":l,f=e.bidding,g=void 0!==f&&f,b=e.iframeBidders,h=void 0===b?["openx"]:b,v=e.others,m=void 0===v?{}:v,w=[],y=!1,A=function e(n){return Array.isArray(n)?1+Math.max.apply(Math,u(n.map(function(n){return e(n)}))):0}(r);r&&void 0!==r&&1===A?w.push.apply(w,u(r)):r&&void 0!==r&&r.length>0&&2===A?w.push.apply(w,u(r)):r&&r.forEach(function(e){w.push.apply(w,u(e))});try{if(!(c&&c.hasOwnProperty("position")||!1===o)){var P=this.positions[o]+1||1;this.positions[o]=P;var k=Object.assign(c,{position:P});Object.assign(e,{targeting:k})}if(isMobile.any()&&"mobile"===p||!isMobile.any()&&"desktop"===p||"all"===p){if(g.prebid&&g.prebid.bids&&this.wrapper.prebid&&this.wrapper.prebid.enabled&&w){pbjs&&h.length>0&&pbjs.setConfig({userSync:{iframeEnabled:!0,filterSettings:{iframe:{bidders:h,filter:"include"}}}});var z=this.wrapper.prebid.useSlotForAdUnit?(0,a.determineSlotName)(this.dfpId,i):n;d.queuePrebidCommand.bind(this,(0,d.addUnit)(z,w,g.prebid.bids,this.wrapper.prebid,m))}(y=this.displayAd.bind(this,e))&&(0,a.queueGoogletagCommand)(y)}}catch(e){console.error("ads error",e)}}},{key:"registerAdCollection",value:function(e){var n=this;e.forEach(function(e){n.registerAd(e)})}},{key:"registerAdCollectionSingleCall",value:function(e){var n=this;window.blockArcAdsLoad=!0,window.blockArcAdsPrebid=!0,e.forEach(function(e){n.registerAd(e)}),window.blockArcAdsLoad=!1,window.blockArcAdsPrebid=!1,pbjs.requestBids({timeout:BIDDER_TIMEOUT||700,bidsBackHandler:function(e){console.log("Bid Back Handler",e),pbjs.setTargetingForGPTAsync(),window.googletag.pubads().refresh()}})}},{key:"displayAd",value:function(e){var n=e.id,i=e.slotName,r=e.dimensions,t=e.targeting,d=e.sizemap,u=void 0!==d&&d,c=e.bidding,l=void 0!==c&&c,p=e.prerender,f=void 0===p?null:p,g=(0,a.determineSlotName)(this.dfpId,i),b=r&&!r.length?null:r,h=r?window.googletag.defineSlot(g,b,n):window.googletag.defineOutOfPageSlot(g,n);if(u&&u.breakpoints&&r){var v=(0,s.prepareSizeMaps)(b,u.breakpoints),m=v.mapping,w=v.breakpoints,y=v.correlators;if(!h)return!1;h.defineSizeMapping(m),u.refresh&&(0,s.setResizeListener)({ad:h,slotName:g,breakpoints:w,id:n,mapping:m,correlators:y,bidding:l,wrapper:this.wrapper,prerender:f})}h&&(h.addService(window.googletag.pubads()),(0,a.setTargeting)(h,t));var A=u&&u.breakpoints?u.breakpoints:[];r&&l&&(l.amazon&&l.amazon.enabled||l.prebid&&l.prebid.enabled)?(0,o.fetchBids)({ad:h,id:n,slotName:g,dimensions:b,wrapper:this.wrapper,prerender:f,bidding:l,breakpoints:A}):window.blockArcAdsPrebid||(0,a.refreshSlot)({ad:h,prerender:f,info:{adUnit:h,adSlot:g,adDimensions:b,adId:n}})}}]),e}()},function(e,n,i){"use strict";Object.defineProperty(n,"__esModule",{value:!0});var r=function(){function e(e,n){for(var i=0;i1}},{key:"any",value:function(){return this.Android()||this.Kindle()||this.KindleFire()||this.Silk()||this.BlackBerry()||this.iOS()||this.Windows()||this.FirefoxOS()}}]),e}();n.default=t},function(e,n,i){"use strict";Object.defineProperty(n,"__esModule",{value:!0}),n.appendResource=function(e,n,i,r,t){var o=document.createElement(e);if("script"!==e)return;o.src=n,o.async=i||!1,o.defer=i||r||!1;(document.head||document.documentElement).appendChild(o),t&&t()}},function(e,n,i){"use strict";Object.defineProperty(n,"__esModule",{value:!0}),n.expandQueryString=function(e){var n=window.location.href,i=e.replace(/[[\]]/g,"\\$&"),r=new RegExp("[?&]"+i+"(=([^&#]*)|&|#|$)").exec(n);if(!r)return null;if(!r[2])return"";return decodeURIComponent(r[2].replace(/\+/g," "))}},function(e,n,i){"use strict";function r(e){window.apstag&&e()}Object.defineProperty(n,"__esModule",{value:!0}),n.fetchAmazonBids=function(e,n,i,t){var o=arguments.length>4&&void 0!==arguments[4]?arguments[4]:null,a=i;if(t&&void 0!==window.innerWidth&&void 0!==i[0][0][0]){for(var d=window.innerWidth,s=-1,u=t.length,c=0;c=t[c][0]){s=c;break}a=i[s]}r(function(){var i={slotName:n,slotID:e,sizes:a};window.apstag.fetchBids({slots:[i]},function(){window.apstag.setDisplayBids(),o&&o()})})},n.queueAmazonCommand=r},function(e,n,i){"use strict";Object.defineProperty(n,"__esModule",{value:!0}),n.resizeListeners=n.sizemapListeners=void 0,n.prepareSizeMaps=function(e,n){var i=[],r=[],t=[];return(n.length?n:null).forEach(function(n,o){i.push([n,e[o]]),-1===r.indexOf(n[0])&&(r.push(n[0]),t.push(!1))}),r.sort(function(e,n){return e-n}),{mapping:i,breakpoints:r,correlators:t}},n.parseSizeMappings=s,n.runResizeEvents=u,n.setResizeListener=function(e){var n=e.id,i=e.correlators;d[n]=(0,r.debounce)(u(e),250),window.addEventListener("resize",d[n]),a[n]={listener:d[n],correlators:i}};var r=i(9),t=i(1),o=i(0),a=n.sizemapListeners={},d=n.resizeListeners={};function s(e){try{var n=[window.innerWidth||document.documentElement.clientWidth||document.body.clientWidth,window.innerHeight||document.documentElement.clientHeight||document.body.clientHeight],i=e.filter(function(e){return e[0][0]<=n[0]&&e[0][1]<=n[1]}),r=i.length>0?i[0][1]:[];return r.length>0&&r[0].constructor!==Array&&(r=[r]),r}catch(n){return e[e.length-1][1]}}function u(e){var n=void 0,i=!1;return function(){for(var r=e.ad,d=e.breakpoints,u=e.id,c=e.bidding,l=e.mapping,p=e.slotName,f=e.wrapper,g=e.prerender,b=window.innerWidth,h=void 0,v=void 0,m=0;mh&&(b { + beforeEach(() => { + global.amazonTest = { + cmd: () => jest.fn().mockName('cmd'), + queueAmazonCommand: () => jest.fn().mockName('queueAmazonCommand'), + }; + }); + + afterEach(() => { + window.apstag = false; + jest.restoreAllMocks(); + }); + + it('fetchAmazonBids', () => { + window.innerWidth = '1280'; + window.apstag = { + fetchBids: () => jest.fn(), + }; + const mockSpy = jest.spyOn(window.apstag, 'fetchBids'); + fetchAmazonBids( + 'id', 'slotname', + [ + [[728, 90], 90], + [728, 90], + [468, 60] + ], [[728, 90], 1080] + ); + expect(mockSpy).toHaveBeenCalled(); + }); + + it('call passed in function in queueAmazonCommand', () => { + window.apstag = true; + const spy = jest.spyOn(amazonTest, 'cmd'); + queueAmazonCommand(global.amazonTest.cmd); + expect(spy).toHaveBeenCalled(); + }); +}); \ No newline at end of file diff --git a/src/__tests__/arcads.test.js b/src/__tests__/arcads.test.js index ee0a08d..56d6a4e 100644 --- a/src/__tests__/arcads.test.js +++ b/src/__tests__/arcads.test.js @@ -1,6 +1,13 @@ import { ArcAds } from '../index'; +import * as gptService from '../services/gpt.js'; +import * as prebidService from '../services/prebid.js'; describe('arcads', () => { + + beforeEach(() => { + jest.clearAllMocks(); + }); + const arcAds = new ArcAds({ dfp: { id: '123' @@ -16,7 +23,9 @@ describe('arcads', () => { } }); - describe('#constructor', () => { + let registerAdCollectionSingleCallMock; + + describe('constructor', () => { it('should initialize arc ads', () => { expect(arcAds).not.toBeUndefined(); }); @@ -30,5 +39,141 @@ describe('arcads', () => { const { arcBiddingReady } = global; expect(arcBiddingReady).toBeDefined(); }); + + it('should console warn if no dfpID provided', () => { + const consoleMock = jest.fn(); + console.warn = consoleMock; + const arcAds = new ArcAds({ + dfp: { + } + }); + + expect(consoleMock).toHaveBeenCalledTimes(1); + expect(consoleMock).toHaveBeenCalledWith("ArcAds: DFP id is missing from the arcads initialization script.", '\n', + "Documentation: https://github.com/wapopartners/arc-ads#getting-started"); + }); }); + + describe('registerAdCollection', () => { + it('calls registerAd for each advert in the collection param', () => { + + const registerAdMock = jest.fn(); + arcAds.registerAd = registerAdMock; + + const adCollection = ['ad1', 'ad2']; + arcAds.registerAdCollection(adCollection); + + expect(registerAdMock).toHaveBeenCalledTimes(2); + }); + }); + + describe('registerAdCollectionSingleCall', () => { + it('calls registerAd, requestBids, refresh', () => { + + const registerAdMock = jest.fn(); + arcAds.registerAd = registerAdMock; + + const refreshMock = jest.fn(); + window.googletag.pubads = jest.fn().mockReturnValue({refresh: refreshMock}); + + const requestBidsMock = jest.fn(); + global.pbjs = {requestBids: requestBidsMock}; + + const adCollection = ['ad1', 'ad2']; + + arcAds.registerAdCollectionSingleCall(adCollection); + + + expect(registerAdMock).toHaveBeenCalledTimes(2); + expect(refreshMock).toHaveBeenCalledTimes(0); + expect(requestBidsMock).toHaveBeenCalledTimes(1); + }); + }); + + describe('setAdsBlockGate', () => { + + it('sets blockArcAdsLoad to true if has window', () => { + ArcAds.setAdsBlockGate(); + expect(window.blockArcAdsLoad).toEqual(true); + }); + + it('does nothing if no window', () => { + const saveGetWindow = ArcAds.getWindow; + const getWindowMock = jest.fn().mockReturnValue(undefined); + ArcAds.getWindow = getWindowMock; + + ArcAds.setAdsBlockGate(); + expect(getWindowMock()).toEqual(undefined); + ArcAds.getWindow = saveGetWindow; + }); + }); + + describe('releaseAdsBlockGate', () => { + + it('sets blockArcAdsLoad to false if has window', () => { + ArcAds.releaseAdsBlockGate(); + expect(window.blockArcAdsLoad).toEqual(false); + }); + + it('does nothing if no window', () => { + const getWindowMock = jest.fn().mockReturnValue(undefined); + ArcAds.getWindow = getWindowMock; + + ArcAds.releaseAdsBlockGate(); + expect(getWindowMock()).toEqual(undefined); + getWindowMock.mockRestore(); + }); + }); + + describe('sendSingleCallAds', () => { + + beforeAll(() => { + registerAdCollectionSingleCallMock = jest.fn(); + arcAds.registerAdCollectionSingleCall = registerAdCollectionSingleCallMock; + }); + + it('if has nothing in adsList return', () => { + arcAds.adsList = []; + const result = arcAds.sendSingleCallAds(); + expect(result).toEqual(false); + }); + + it('if has adsList elems and pubads do SRA call', () => { + arcAds.adsList = ['ad1', 'ad2']; + window.googletag.pubadsReady = true; + window.googletag.pubads =jest.fn().mockReturnValue({ + disableInitialLoad: jest.fn(), + enableSingleRequest: jest.fn(), + enableAsyncRendering: jest.fn(), + }); + + arcAds.sendSingleCallAds(); + expect(registerAdCollectionSingleCallMock).toHaveBeenCalledTimes(1); + }); + }); + + describe('reserveAd', () => { + it('sets block and adds ad to adsList', () => { + const gateSetSpy = jest.spyOn(ArcAds, 'setAdsBlockGate'); + arcAds.adsList = []; + arcAds.reserveAd({example: true}); + expect(gateSetSpy).toHaveBeenCalledTimes(1); + expect(arcAds.adsList.length).toEqual(1); + }); + }); + + describe('setPageLeveTargeting', () => { + it('sets block and adds ad to adsList', () => { + const setTargetingMock = jest.fn(); + + window.googletag.pubads =jest.fn().mockReturnValue({ + setTargeting: setTargetingMock, + }); + + arcAds.setPageLeveTargeting('testKey', 'testValue'); + expect(setTargetingMock).toHaveBeenCalledTimes(1); + expect(setTargetingMock).toHaveBeenCalledWith('testKey', 'testValue'); + }); + }); + }); diff --git a/src/__tests__/displayAd.test.js b/src/__tests__/displayAd.test.js new file mode 100644 index 0000000..75eedb1 --- /dev/null +++ b/src/__tests__/displayAd.test.js @@ -0,0 +1,231 @@ +import { ArcAds } from '../index'; +import * as gptService from '../services/gpt.js'; +import * as sizemappingService from '../services/sizemapping.js' +import * as headerBidding from '../services/headerbidding.js'; + +describe('displayAd ', () => { + + beforeEach(() => { + jest.clearAllMocks(); + }); + + const defineOutOfPageSlotMock = jest.fn(); + const defineSlotMock = jest.fn(); + + window.googletag= { + defineOutOfPageSlot: defineOutOfPageSlotMock, + defineSlot: defineSlotMock, + pubads: jest.fn(), + }; + const refreshSlotSpy = jest.spyOn(gptService, 'refreshSlot'); + const setResizeListenerSpy = jest.spyOn(sizemappingService, 'setResizeListener'); + const fetchBidsSpy = jest.spyOn(headerBidding, 'fetchBids'); + + + const arcAds = new ArcAds({ + dfp: { + id: '123' + }, + bidding: { + amazon: { + enabled: true, + id: '123' + }, + prebid: { + enabled: true + } + } + }); + + it('if does not have dimensions should call defineOutOfPageSlot', () => { + const adParams = { + id: "testID", + slotName: 'testSlotname', + dimensions: null, + targeting: null, + sizemap: {breakpoints:[0, 50]}, + bidding: false, + prerender: null, + }; + window.blockArcAdsPrebid = false; + const result = arcAds.displayAd(adParams); + + expect(result).toEqual(undefined); + expect(defineOutOfPageSlotMock).toHaveBeenCalledTimes(1); + expect(defineOutOfPageSlotMock).toHaveBeenCalledWith("/123/testSlotname", "testID"); + expect(defineSlotMock).toHaveBeenCalledTimes(0); + + expect(refreshSlotSpy).toHaveBeenCalledTimes(1); + const expectedRefreshSLotParams = {"ad": undefined, "info": {"adDimensions": null, "adId": "testID", "adSlot": "/123/testSlotname", "adUnit": undefined}, "prerender": null}; + expect(refreshSlotSpy).toHaveBeenCalledWith(expectedRefreshSLotParams); + + }); + + it('if no ad object return false', () => { + const adParams = { + id: "testID", + slotName: 'testSlotname', + dimensions: [100,40], + targeting: null, + sizemap: {breakpoints:[0, 50]}, + bidding: false, + prerender: null, + }; + + defineOutOfPageSlotMock.mockReturnValue(null); + window.blockArcAdsPrebid = false; + const result = arcAds.displayAd(adParams); + + expect(result).toEqual(false); + expect(defineOutOfPageSlotMock).toHaveBeenCalledTimes(0); + expect(defineSlotMock).toHaveBeenCalledTimes(1); + expect(defineSlotMock).toHaveBeenCalledWith( "/123/testSlotname", [100, 40], "testID"); + + expect(refreshSlotSpy).toHaveBeenCalledTimes(0); + }); + + it('if sizemap.refresh call resize listener', () => { + const adParams = { + id: "testID", + slotName: 'testSlotname', + dimensions: [100,40], + targeting: null, + sizemap: {breakpoints:[0, 50], refresh: true}, + bidding: false, + prerender: null, + }; + + window.blockArcAdsPrebid = true; + + const defineSizeMappingMock = jest.fn(); + defineSlotMock.mockReturnValue({ + defineSizeMapping: defineSizeMappingMock, + addService: jest.fn() + }); + const result = arcAds.displayAd(adParams); + + expect(result).toEqual(undefined); + expect(defineOutOfPageSlotMock).toHaveBeenCalledTimes(0); + + expect(defineSlotMock).toHaveBeenCalledTimes(1); + expect(defineSlotMock).toHaveBeenCalledWith( "/123/testSlotname", [100, 40], "testID"); + + expect(refreshSlotSpy).toHaveBeenCalledTimes(0); + + expect(defineSizeMappingMock).toHaveBeenCalledTimes(1); + expect(defineSizeMappingMock).toHaveBeenCalledWith([[0, 100], [50, 40]]); + + expect(setResizeListenerSpy).toHaveBeenCalledTimes(1); + + expect(setResizeListenerSpy.mock.calls[0][0]).toEqual( + expect.objectContaining({ + "bidding": false, + "breakpoints": [undefined], + "correlators": [false], + "id": "testID", + "mapping": [[0, 100], [50, 40]], + "prerender": null, + "slotName": "/123/testSlotname", + "wrapper": {"amazon": {"enabled": true, "id": "123"}, "prebid": {"enabled": true}}} + ) + ); + }); + + it('if no sizemap.refresh, do NOT call resize listener', () => { + const adParams = { + id: "testID", + slotName: 'testSlotname', + dimensions: [100,40], + targeting: null, + sizemap: {breakpoints:[0, 50], refresh: false}, + bidding: false, + prerender: null, + }; + + window.blockArcAdsPrebid = true; + + const defineSizeMappingMock = jest.fn(); + defineSlotMock.mockReturnValue({ + defineSizeMapping: defineSizeMappingMock, + addService: jest.fn() + }); + arcAds.displayAd(adParams); + expect(setResizeListenerSpy).toHaveBeenCalledTimes(0); + }); + + it('if has adsList and ad push ad to adsList', () => { + const adParams = { + id: "testID", + slotName: 'testSlotname', + dimensions: [100,40], + targeting: null, + sizemap: {breakpoints:[0, 50], refresh: false}, + bidding: false, + prerender: null, + }; + + window.blockArcAdsPrebid = true; + window.adsList = []; + + const defineSizeMappingMock = jest.fn(); + defineSlotMock.mockReturnValue({ + defineSizeMapping: defineSizeMappingMock, + addService: jest.fn() + }); + arcAds.displayAd(adParams); + expect(window.adsList.length).toEqual(1); + }); + + it('if has bidding.prebid.enabled call fetchBids', () => { + const adParams = { + id: "testID", + slotName: 'testSlotname', + dimensions: [100,40], + targeting: null, + sizemap: {breakpoints:[0, 50], refresh: false}, + bidding: {prebid: {enabled: true}}, + prerender: null, + }; + + window.blockArcAdsPrebid = false; + arcAds.displayAd(adParams); + + expect(refreshSlotSpy).toHaveBeenCalledTimes(0); + expect(fetchBidsSpy).toHaveBeenCalledTimes(1); + }); + + it('handles non-null dimnsions length 0 case', () => { + const adParams = { + id: "testID", + slotName: 'testSlotname', + dimensions: [], + targeting: null, + sizemap: {breakpoints:[0, 50], refresh: false}, + bidding: {prebid: {enabled: true}}, + prerender: null, + }; + + const defineSizeMappingMock = jest.fn(); + defineSlotMock.mockReturnValue({ + defineSizeMapping: defineSizeMappingMock, + addService: jest.fn() + }); + + const result = arcAds.displayAd(adParams); + + expect(result).toEqual(undefined); + expect(defineOutOfPageSlotMock).toHaveBeenCalledTimes(0); + + expect(defineSlotMock).toHaveBeenCalledTimes(1); + expect(defineSlotMock).toHaveBeenCalledWith( "/123/testSlotname", null, "testID"); + + expect(refreshSlotSpy).toHaveBeenCalledTimes(0); + + expect(defineSizeMappingMock).toHaveBeenCalledTimes(1); + expect(defineSizeMappingMock).toHaveBeenCalledWith([]); + + expect(setResizeListenerSpy).toHaveBeenCalledTimes(0); + + }); + +}); \ No newline at end of file diff --git a/src/__tests__/gpt.test.js b/src/__tests__/gpt.test.js index 4483df5..489320e 100644 --- a/src/__tests__/gpt.test.js +++ b/src/__tests__/gpt.test.js @@ -2,6 +2,7 @@ import { ArcAds } from '../index'; import * as gpt from '../services/gpt'; import * as headerbidding from '../services/headerbidding'; import * as sizemap from '../services/sizemapping'; +import * as queryUtil from '../util/query'; describe('arcads', () => { const methods = { @@ -104,4 +105,102 @@ describe('arcads', () => { expect(methods.setResizeListener.mock.calls.length).toBe(1); }); }); + + it('if has prerender that resolves call refresh', () => { + window.googletag.pubadsReady = true; + const prerenderFnc = jest.fn(); + gpt.refreshSlot({ad:{name:"ad"}, correlator:false, prerender:prerenderFnc, info:{}}); + expect(prerenderFnc).toHaveBeenCalledTimes(1); + }); + + it('if blockarcAds load is set do not call pubads refresh', () => { + window.googletag.pubadsReady = true; + window.blockArcAdsLoad = true; + const prerenderFnc = jest.fn(); + + const refreshMock = jest.fn(); + global.googletag.pubads = jest.fn().mockReturnValue({ + refresh: refreshMock, + }); + gpt.refreshSlot({ad:{name:"ad"}, correlator:false, prerender:prerenderFnc, info:{}}); + expect(refreshMock).toHaveBeenCalledTimes(0); + }); + + describe('setTargeting', () => { + it('if options has key and value call ad SetTargeting', () => { + const setTargetingMock = jest.fn(); + const ad = {setTargeting: setTargetingMock}; + gpt.setTargeting(ad, {testKey:"testValue"}); + expect(setTargetingMock).toHaveBeenCalledTimes(1); + expect(setTargetingMock).toHaveBeenCalledWith("testKey","testValue" ); + }); + + it('if options has NOT key and value call ad SetTargeting', () => { + const setTargetingMock = jest.fn(); + const ad = {setTargeting: setTargetingMock}; + gpt.setTargeting(ad, {testKey:null}); + expect(setTargetingMock).toHaveBeenCalledTimes(0); + }); + }); + + describe('dfpSettings', () => { + + const disableInitialLoadMock = jest.fn(); + const enableSingleRequestMock = jest.fn(); + const enableAsyncRenderingMock = jest.fn(); + const enableServicesMock = jest.fn(); + const collapseEmptyDivsMock = jest.fn(); + const addEventListenerMock = jest.fn(); + + beforeAll(() => { + global.googletag.pubads = jest.fn().mockReturnValue({ + disableInitialLoad: disableInitialLoadMock, + enableSingleRequest : enableSingleRequestMock, + enableAsyncRendering: enableAsyncRenderingMock, + collapseEmptyDivs: collapseEmptyDivsMock, + addEventListener: addEventListenerMock, + }); + + global.googletag.enableServices = enableServicesMock; + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('call non-logic dependent pubads setup functions', () => { + gpt.dfpSettings(); + expect(disableInitialLoadMock).toHaveBeenCalledTimes(1); + expect(enableSingleRequestMock).toHaveBeenCalledTimes(1); + expect(enableAsyncRenderingMock).toHaveBeenCalledTimes(1); + expect(enableServicesMock).toHaveBeenCalledTimes(1); + expect(enableAsyncRenderingMock).toHaveBeenCalledTimes(1); + expect(collapseEmptyDivsMock).toHaveBeenCalledTimes(0); + expect(addEventListenerMock).toHaveBeenCalledTimes(0); + }); + + it( 'if handleSlotRenderEnded function calls addEventListener', () => { + const handleMock = jest.fn(); + gpt.dfpSettings(handleMock); + expect(disableInitialLoadMock).toHaveBeenCalledTimes(1); + expect(enableSingleRequestMock).toHaveBeenCalledTimes(1); + expect(enableAsyncRenderingMock).toHaveBeenCalledTimes(1); + expect(enableServicesMock).toHaveBeenCalledTimes(1); + expect(enableAsyncRenderingMock).toHaveBeenCalledTimes(1); + expect(addEventListenerMock).toHaveBeenCalledTimes(1); + }); + }); + + describe('determineSlotName', () => { + it('return slotname based on dfpCode and slotname args', () => { + const result = gpt.determineSlotName('dfpCode',"testSlotname"); + expect(result).toEqual('/dfpCode/testSlotname'); + }); + + it('ifAdsSlot override then use that value for slotaName', () => { + jest.spyOn(queryUtil, 'expandQueryString').mockReturnValue('overrideSlotname'); + const result = gpt.determineSlotName('dfpCode',"testSlotname"); + expect(result).toEqual('/dfpCode/overrideSlotname'); + }); + }); }); diff --git a/src/__tests__/headerbidding.test.js b/src/__tests__/headerbidding.test.js new file mode 100644 index 0000000..e49873c --- /dev/null +++ b/src/__tests__/headerbidding.test.js @@ -0,0 +1,118 @@ +import { + initializeBiddingServices + } from '../services/headerbidding'; + + describe('initializeBiddingServices', function () { + afterEach(() => { + window.apstag = false; + window.arcBiddingReady = true; + jest.restoreAllMocks(); + }); + + it('return while window.arcBiddingReady is already set in initializeBiddingServices', () => { + Object.defineProperty(window, 'arcBiddingReady', { + writable: true, + value: true, + }); + const mockSetting = { + prebid: false, + amazon: false, + }; + const result = initializeBiddingServices(mockSetting); + expect(result).toEqual(undefined); + }); + + it('set arcBiddingReady to false while no prebid or amazon were set in initializeBiddingServices', () => { + Object.defineProperty(window, 'arcBiddingReady', { + writable: true, + value: false, + }); + const mockSetting = { + prebid: false, + amazon: false, + }; + initializeBiddingServices(mockSetting); + expect(window.arcBiddingReady).toEqual(false); + }); + + it('enable prebid ', () => { + Object.defineProperty(window, 'arcBiddingReady', { + writable: true, + value: false, + }); + Object.defineProperty(window, 'apstag', { + writable: true, + value: true, + }); + + const mockSetting = { + prebid: { + enabled: true, + }, + amazon: { + enabled: false, + } + }; + + initializeBiddingServices(mockSetting); + expect(window.arcBiddingReady).toEqual(false); + }); + + it('arcBiddingReady set to false while no pbjs available', () => { + global.pbjs = undefined; + const mockSetting = { + prebid: { + enabled: true, + }, + }; + initializeBiddingServices(mockSetting); + setTimeout(() => { + expect(window.arcBiddingReady).toEqual(false); + }, 2000); + }); + + it('enable Amazon ', () => { + Object.defineProperty(window, 'arcBiddingReady', { + writable: true, + value: false, + }); + Object.defineProperty(window, 'apstag', { + writable: true, + value: true, + }); + const mockSetting = { + prebid: false, + amazon: { + enabled: true, + id: 'mock-id' + }, + }; + initializeBiddingServices(mockSetting); + setTimeout(() => { + expect(window.arcBiddingReady).toEqual(false); + }, 2000); + }); + + it('enable Amazon without id ', () => { + Object.defineProperty(window, 'arcBiddingReady', { + writable: true, + value: false, + }); + Object.defineProperty(window, 'apstag', { + writable: true, + value: true, + }); + const mockSetting = { + prebid: false, + amazon: { + enabled: true, + id: '' + }, + }; + initializeBiddingServices(mockSetting); + + setTimeout(() => { + expect(window.arcBiddingReady).toEqual(false); + }, 2000); + }); + }); \ No newline at end of file diff --git a/src/__tests__/mobile.test.js b/src/__tests__/mobile.test.js new file mode 100644 index 0000000..97b14c6 --- /dev/null +++ b/src/__tests__/mobile.test.js @@ -0,0 +1,479 @@ + +import {MobileDetection} from '../util/mobile.js'; + +describe('MobileDetection', () => { + afterAll(() => { + window.__defineGetter__('navigator', function () { + return {}; + }); + + window.navigator.__defineGetter__('userAgent', function () { + return null; + }); + + window.__defineGetter__('retina', function () { + return false; + }); + + window.__defineGetter__('devicePixelRatio', function () { + return 0; + }); + + }); + + describe('Android()', () => { + it ('returns true if user agent contains Android', () => { + window.navigator.__defineGetter__('userAgent', function () { + return 'Android'; + }); + + const result = MobileDetection.Android(); + expect(result).toEqual(true); + }); + + it ('returns false if user agent does not contains Android', () => { + + window.navigator.__defineGetter__('userAgent', function () { + return 'that which shall not be named'; + }); + + const result = MobileDetection.Android(); + expect(result).toEqual(false); + }); + + }); + + describe('AndroidOld()', () => { + + it ('returns true if user agent contains Android 2.3.3', () => { + + window.navigator.__defineGetter__('userAgent', function () { + return 'Android 2.3.3'; + }); + + const result = MobileDetection.AndroidOld(); + expect(result).toEqual(true); + }); + + it ('returns false if user agent does not contain Android 2.3.3', () => { + + window.navigator.__defineGetter__('userAgent', function () { + return 'that which shall not be named'; + }); + + const result = MobileDetection.AndroidOld(); + expect(result).toEqual(false); + }); + + }); + + describe('AndroidTablet()', () => { + + it ('returns true if user agent contains Android Mobile', () => { + + window.navigator.__defineGetter__('userAgent', function () { + return 'Android'; + }); + + const result = MobileDetection.AndroidTablet(); + expect(result).toEqual(true); + }); + + it ('returns false if user agent does not contain Android Mobile', () => { + + window.navigator.__defineGetter__('userAgent', function () { + return 'that which shall not be named'; + }); + + const result = MobileDetection.AndroidTablet(); + expect(result).toEqual(false); + }); + + }); + + describe('Kindle()', () => { + + it ('returns true if user agent contains Kindle', () => { + + window.navigator.__defineGetter__('userAgent', function () { + return 'Kindle'; + }); + + const result = MobileDetection.Kindle(); + expect(result).toEqual(true); + }); + + it ('returns false if user agent does not contain Kindle', () => { + + window.navigator.__defineGetter__('userAgent', function () { + return 'that which shall not be named'; + }); + + const result = MobileDetection.Kindle(); + expect(result).toEqual(false); + }); + + }); + + describe('KindleFire()', () => { + + it ('returns true if user agent contains KFOT', () => { + + window.navigator.__defineGetter__('userAgent', function () { + return 'KFOT'; + }); + + const result = MobileDetection.KindleFire(); + expect(result).toEqual(true); + }); + + it ('returns false if user agent does not contain Kindle', () => { + + window.navigator.__defineGetter__('userAgent', function () { + return 'that which shall not be named'; + }); + + const result = MobileDetection.KindleFire(); + expect(result).toEqual(false); + }); + + }); + + + describe('Silk()', () => { + + it ('returns true if user agent contains Silk', () => { + + window.navigator.__defineGetter__('userAgent', function () { + return 'Silk'; + }); + + const result = MobileDetection.Silk(); + expect(result).toEqual(true); + }); + + it ('returns false if user agent does not contain Silk', () => { + + window.navigator.__defineGetter__('userAgent', function () { + return 'that which shall not be named'; + }); + + const result = MobileDetection.Silk(); + expect(result).toEqual(false); + }); + + }); + + describe('BlackBerry()', () => { + + it ('returns true if user agent contains BlackBerry', () => { + + window.navigator.__defineGetter__('userAgent', function () { + return 'BlackBerry'; + }); + + const result = MobileDetection.BlackBerry(); + expect(result).toEqual(true); + }); + + it ('returns false if user agent does not contain BlackBerry', () => { + + window.navigator.__defineGetter__('userAgent', function () { + return 'that which shall not be named'; + }); + + const result = MobileDetection.BlackBerry(); + expect(result).toEqual(false); + }); + + }); + + describe('iOS()', () => { + + it ('returns true if user agent contains iPhone', () => { + + window.navigator.__defineGetter__('userAgent', function () { + return 'iPhone'; + }); + + const result = MobileDetection.iOS(); + expect(result).toEqual(true); + }); + + it ('returns true if user agent contains iPad', () => { + + window.navigator.__defineGetter__('userAgent', function () { + return 'iPad'; + }); + + const result = MobileDetection.iOS(); + expect(result).toEqual(true); + }); + + it ('returns true if user agent contains iPod', () => { + + window.navigator.__defineGetter__('userAgent', function () { + return 'iPod'; + }); + + const result = MobileDetection.iOS(); + expect(result).toEqual(true); + }); + + it ('returns false if user agent does not contain iPhone/iPad/iPod', () => { + + window.navigator.__defineGetter__('userAgent', function () { + return 'that which shall not be named'; + }); + + const result = MobileDetection.iOS(); + expect(result).toEqual(false); + }); + + }); + + describe('iPhone()', () => { + + it ('returns true if user agent contains iPhone', () => { + + window.navigator.__defineGetter__('userAgent', function () { + return 'iPhone'; + }); + + const result = MobileDetection.iPhone(); + expect(result).toEqual(true); + }); + + + + it ('returns true if user agent contains iPod', () => { + + window.navigator.__defineGetter__('userAgent', function () { + return 'iPod'; + }); + + const result = MobileDetection.iPhone(); + expect(result).toEqual(true); + }); + + it ('returns false if user agent does not contain iPhone/iPad/iPod', () => { + + window.navigator.__defineGetter__('userAgent', function () { + return 'iPad'; + }); + + const result = MobileDetection.iPhone(); + expect(result).toEqual(false); + }); + + }); + + describe('iPad()', () => { + + it ('returns true if user agent contains iPad', () => { + + window.navigator.__defineGetter__('userAgent', function () { + return 'iPad'; + }); + + const result = MobileDetection.iPad(); + expect(result).toEqual(true); + }); + + it ('returns false if user agent does not contain iPad', () => { + + window.navigator.__defineGetter__('userAgent', function () { + return 'that which shall not be named'; + }); + + const result = MobileDetection.iPad(); + expect(result).toEqual(false); + }); + + }); + + describe('Windows()', () => { + + it ('returns true if user agent contains IEMobile', () => { + + window.navigator.__defineGetter__('userAgent', function () { + return 'IEMobile'; + }); + + const result = MobileDetection.Windows(); + expect(result).toEqual(true); + }); + + it ('returns false if user agent does not contain IEMobile', () => { + + window.navigator.__defineGetter__('userAgent', function () { + return 'that which shall not be named'; + }); + + const result = MobileDetection.Windows(); + expect(result).toEqual(false); + }); + + }); + + describe('FirefoxOS()', () => { + + it ('returns true if user agent contains Mozilla and Mobile', () => { + + window.navigator.__defineGetter__('userAgent', function () { + return 'Mozilla Mobile'; + }); + + const result = MobileDetection.FirefoxOS(); + expect(result).toEqual(true); + }); + + it ('returns false if user agent contains Mozilla and not Mobile', () => { + + window.navigator.__defineGetter__('userAgent', function () { + return 'Mozilla'; + }); + + const result = MobileDetection.FirefoxOS(); + expect(result).toEqual(false); + }); + + it ('returns false if user agent contains not Mozilla but Mobile', () => { + + window.navigator.__defineGetter__('userAgent', function () { + return 'Mobile'; + }); + + const result = MobileDetection.FirefoxOS(); + expect(result).toEqual(false); + }); + + it ('returns false if user agent contains neither Mozilla or Mobile', () => { + + window.navigator.__defineGetter__('userAgent', function () { + return 'that which shall not be named'; + }); + + const result = MobileDetection.FirefoxOS(); + expect(result).toEqual(false); + }); + + }); + + describe('any()', () => { + + it ('returns true if user agent contains Android', () => { + + window.navigator.__defineGetter__('userAgent', function () { + return 'Android'; + }); + + const result = MobileDetection.any(); + expect(result).toEqual(true); + }); + + it ('returns true if user agent contains Kindle', () => { + + window.navigator.__defineGetter__('userAgent', function () { + return 'Kindle'; + }); + + const result = MobileDetection.any(); + expect(result).toEqual(true); + }); + + it ('returns true if user agent contains KindleFire', () => { + + window.navigator.__defineGetter__('userAgent', function () { + return 'KFOT'; + }); + + const result = MobileDetection.any(); + expect(result).toEqual(true); + }); + + it ('returns true if user agent contains Silk', () => { + + window.navigator.__defineGetter__('userAgent', function () { + return 'Silk'; + }); + + const result = MobileDetection.any(); + expect(result).toEqual(true); + }); + + it ('returns true if user agent contains BlackBerry', () => { + + window.navigator.__defineGetter__('userAgent', function () { + return 'BlackBerry'; + }); + + const result = MobileDetection.any(); + expect(result).toEqual(true); + }); + + it ('returns true if user agent contains iPad', () => { + + window.navigator.__defineGetter__('userAgent', function () { + return 'iPad'; + }); + + const result = MobileDetection.any(); + expect(result).toEqual(true); + }); + + it ('returns true if user agent contains iPod', () => { + + window.navigator.__defineGetter__('userAgent', function () { + return 'iPod'; + }); + + const result = MobileDetection.any(); + expect(result).toEqual(true); + }); + + it ('returns true if user agent contains iPhone', () => { + + window.navigator.__defineGetter__('userAgent', function () { + return 'iPhone'; + }); + + const result = MobileDetection.any(); + expect(result).toEqual(true); + }); + + it ('returns true if user agent contains IEMobile', () => { + + window.navigator.__defineGetter__('userAgent', function () { + return 'IEMobile'; + }); + + const result = MobileDetection.any(); + expect(result).toEqual(true); + }); + + it ('returns true if user agent contains Mozilla Mobile', () => { + + window.navigator.__defineGetter__('userAgent', function () { + return 'Mozilla Mobile'; + }); + + const result = MobileDetection.any(); + expect(result).toEqual(true); + }); + + it ('returns false if user agent contains invalid mobile userAgent', () => { + + window.navigator.__defineGetter__('userAgent', function () { + return 'no no no'; + }); + + const result = MobileDetection.any(); + expect(result).toEqual(false); + }); + + + + }); + +}); diff --git a/src/__tests__/prebid.test.js b/src/__tests__/prebid.test.js new file mode 100644 index 0000000..54b730e --- /dev/null +++ b/src/__tests__/prebid.test.js @@ -0,0 +1,84 @@ +import { + queuePrebidCommand, + fetchPrebidBids, + addUnit, + fetchPrebidBidsArray, +} from '../services/prebid'; + +const setpbjs = () => { + global.pbjs = { + que: [], + addAdUnits: () => jest.fn().mockName('addAdUnits'), + requestBids: () => jest.fn().mockName('requestBids').mockReturnValueOnce('result'), + setConfig: () => jest.fn().mockName('setConfig'), + bidsBackHandler: () => jest.fn().mockName('bidsBackHandler'), + setTargetingForGPTAsync: jest.fn().mockName('setTargetingForGPTAsync'), + }; +}; + +describe('pbjs', () => { + const info = { + bids: [], + }; + + beforeEach(() => { + setpbjs(); + }); + + afterEach(() => { + window.blockArcAdsPrebid = false; + global.pbjs = {}; + jest.restoreAllMocks(); + }); + + it('return if blockArcAdsPrebid is block', () => { + window.blockArcAdsPrebid = true; + const spy = jest.spyOn(pbjs, 'requestBids'); + const mockCb = jest.fn(); + fetchPrebidBidsArray({}, {}, {}, info, {}, mockCb()); + expect(spy).not.toHaveBeenCalled(); + }); + + it('fetchPrebidBidsArray if blockArcAdsPrebid is not block', () => { + const spy = jest.spyOn(pbjs, 'requestBids'); + const mockCb = jest.fn(); + fetchPrebidBidsArray({}, {}, {}, info, {}, mockCb()); + expect(spy).toHaveBeenCalled(); + }); + + it('fetchPrebidBids', () => { + const spy = jest.spyOn(pbjs, 'requestBids'); + fetchPrebidBids({}, {}, {}, info, {}); + expect(spy).toHaveBeenCalled(); + }); + + it('return undefined while window blockArcAdsPrebid is set to true', () => { + window.blockArcAdsPrebid = true; + const result = fetchPrebidBids({}, {}, {}, info, {}); + expect(result).toEqual(undefined); + }); + + it('push fn into queuePrebidCommand', () => { + const mockFn = jest.fn(); + queuePrebidCommand(mockFn); + expect(pbjs.que.length).toEqual(1); + }); + + it('addUnit while slot is available', () => { + const spy = jest.spyOn(pbjs, 'addAdUnits'); + addUnit('', '', {}, {}, {}); + expect(spy).toHaveBeenCalled(); + }); + + it('called config while sizeConfig is passed ', () => { + const spy = jest.spyOn(pbjs, 'setConfig'); + addUnit('', '', {}, { config: { sample: 'test' } }, {}); + expect(spy).toHaveBeenCalled(); + }); + + it('called setConfig while sizeConfig is passed ', () => { + const spy = jest.spyOn(pbjs, 'setConfig'); + addUnit('', '', {}, { sizeConfig: { sample: 'test' } }, {}); + expect(spy).toHaveBeenCalled(); + }); +}); \ No newline at end of file diff --git a/src/__tests__/query.test.js b/src/__tests__/query.test.js new file mode 100644 index 0000000..b81c28a --- /dev/null +++ b/src/__tests__/query.test.js @@ -0,0 +1,34 @@ +import { expandQueryString } from '../util/query.js'; + + +describe('expandQueryString', () => { + + const saveLocation = global.window.location; + + afterAll(() => { + delete global.window.location; + global.window.location = saveLocation; + }); + + it('gets url value for param name passed', () => { + delete global.window.location; + global.window = Object.create(window); + global.window.location = { + href:'http://www.test.com?adslot=hello', + }; + + const result = expandQueryString('adslot'); + expect(result).toEqual('hello'); + }); + + it('if no result return empty string', () => { + delete global.window.location; + global.window = Object.create(window); + global.window.location = { + href:'http://www.test.com?adslot=', + }; + + const result = expandQueryString('adslot'); + expect(result).toEqual(''); + }); + }); \ No newline at end of file diff --git a/src/__tests__/registerAds.test.js b/src/__tests__/registerAds.test.js new file mode 100644 index 0000000..18d84f4 --- /dev/null +++ b/src/__tests__/registerAds.test.js @@ -0,0 +1,336 @@ +import { ArcAds } from '../index'; +import * as gptService from '../services/gpt.js'; +import * as prebidService from '../services/prebid.js'; +import * as mobileDetection from '../util/mobile.js'; + + +describe('registerAds dimensions branches', () => { + + global.pbjs = { + que: [], + addAdUnits: () => jest.fn().mockName('addAdUnits'), + requestBids: () => jest.fn().mockName('requestBids'), + setConfig: () => jest.fn().mockName('setConfig'), + setTargetingForGPTAsync: jest.fn().mockName('setTargetingForGPTAsync'), + }; + + const setConfigSpy = jest.spyOn(pbjs, 'setConfig'); + + //queueGoogletagCommand mock + jest.spyOn(gptService, 'queueGoogletagCommand'); + //const queuePrebidCommandMock = jest.fn(); + const queuePrebidCommandBindMock = jest.fn(); + prebidService.queuePrebidCommand = queuePrebidCommandBindMock; + prebidService.queuePrebidCommand.bind = queuePrebidCommandBindMock; + + // prebidService.queuePrebidCommand.bind = queuePrebidCommandBindMock; + const addUnitMock = jest.fn(); + prebidService.addUnit = addUnitMock; + + const arcAds = new ArcAds({ + dfp: { + id: '123' + }, + bidding: { + amazon: { + enabled: true, + id: '123' + }, + prebid: { + enabled: true + } + } + }); + + //displayAd mock + const displayAdMock = jest.fn(); + const displayAdBindMock = jest.fn().mockReturnValue(jest.fn()); + arcAds.displayAd = displayAdMock; + arcAds.displayAd.bind = displayAdBindMock; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should should call prebid setConfig if has bidding configs and prebid lib is present', () => { + //ArcAds obj + const adParams = { + id: "testID", + slotname: "testSlotname", + dimensions: [[300, 50], [300, 250]] + } + + arcAds.registerAd(adParams); + + expect(gptService.queueGoogletagCommand).toHaveBeenCalledTimes(1); + expect(prebidService.queuePrebidCommand).toHaveBeenCalledTimes(0); + + }); + + it('should add single level dimensions appropriately', () => { + const adParams = { + id: "testID", + slotname: "testSlotname", + dimensions: [300, 50], + } + arcAds.registerAd(adParams); + + expect(arcAds.displayAd.bind).toHaveBeenCalledTimes(1); + + const expectedArg1 = {"adsList": [], + "collapseEmptyDivs": undefined, + "dfpId": "123", + "displayAd": displayAdMock, + "positions": [], + "wrapper": {"amazon": {"enabled": true, "id": "123"}, + "prebid": {"enabled": true}} + }; + const expectedArg2 = {"dimensions": [300, 50], "id": "testID", "slotname": "testSlotname"}; + + expect(displayAdBindMock).toHaveBeenCalledWith(expectedArg1, expectedArg2); + + }); + + it('should add two level dimensions appropriately', () => { + + const adParams = { + id: "testID", + slotname: "testSlotname", + dimensions: [[300, 50], [250, 100]], + } + + arcAds.registerAd(adParams); + + expect(displayAdBindMock).toHaveBeenCalledTimes(1); + + const expectedArg1 = {"adsList": [], + "collapseEmptyDivs": undefined, + "dfpId": "123", + "displayAd": displayAdMock, + "positions": [], + "wrapper": {"amazon": {"enabled": true, "id": "123"}, + "prebid": {"enabled": true}} + }; + const expectedArg2 = {"dimensions": [[300, 50], [250, 100]], "id": "testID", "slotname": "testSlotname"}; + expect(displayAdBindMock).toHaveBeenCalledWith(expectedArg1, expectedArg2); + + }); + + it('should add no dimensions appropriately', () => { + const adParams = { + id: "testID", + slotname: "testSlotname", + dimensions: undefined, + } + arcAds.registerAd(adParams); + + expect(displayAdBindMock).toHaveBeenCalledTimes(1); + + const expectedArg1 = {"adsList": [], + "collapseEmptyDivs": undefined, + "dfpId": "123", + "displayAd": displayAdMock, + "positions": [], + "wrapper": {"amazon": {"enabled": true, "id": "123"}, + "prebid": {"enabled": true}}}; + const expectedArg2 = {"dimensions": undefined, "id": "testID", "slotname": "testSlotname"}; + expect(displayAdBindMock).toHaveBeenCalledWith(expectedArg1, expectedArg2); + }); + + it('should add non 1 or 2 level dimensions appropriately', () => { + const adParams = { + id: "testID", + slotname: "testSlotname", + dimensions: [[[100,50]]], + targeting:{}, + adType: true, + display: 'mobile', + bidding:{prebid:{bids:['bid1']}} + } + + const mobileAny = jest.fn().mockReturnValue(true); + global.isMobile = {any: mobileAny}; + + arcAds.registerAd(adParams); + + expect(setConfigSpy).toHaveBeenCalledTimes(1); + expect(setConfigSpy.mock.calls[0][0]).toEqual( + expect.objectContaining({ + "userSync": { + "filterSettings": { + "iframe": { + "bidders": ["openx"], "filter": "include"} + }, + "iframeEnabled": true + } + }) + ); + + expect(displayAdBindMock).toHaveBeenCalledTimes(1); + const expectedArg2 = { + "adType": true, + "bidding": {"prebid": {"bids": ["bid1"]}}, + "dimensions": [[[100, 50]]], + "display": "mobile", + "id": "testID", + "slotname": "testSlotname", + "targeting": {"position": 1} + }; + expect(displayAdBindMock.mock.calls[0][1]).toEqual( expectedArg2); + }); + + it('wrapper has useSlotForAdUnit for caclulating prebid code', () => { + arcAds.wrapper = { + amazon: { + enabled: true, + id: '123' + }, + prebid: { + enabled: true, + useSlotForAdUnit: true, + } + }; + const adParams = { + id: "testID", + slotname: "testSlotname", + dimensions: [[[100,50]]], + targeting:{}, + adType: true, + display: 'mobile', + bidding:{prebid:{bids:['bid1']}} + } + + const mobileAny = jest.fn().mockReturnValue(true); + global.isMobile = {any: mobileAny}; + + arcAds.registerAd(adParams); + + expect(addUnitMock).toHaveBeenCalledTimes(1); + expect(addUnitMock.mock.calls[0][0]).toEqual("/123/undefined"); + + expect(queuePrebidCommandBindMock).toHaveBeenCalledTimes(1); + }); + + it('handles no display case', () => { + arcAds.wrapper = { + amazon: { + enabled: true, + id: '123' + }, + prebid: { + enabled: true, + useSlotForAdUnit: true, + } + }; + const adParams = { + id: "testID", + slotname: "testSlotname", + dimensions: [[[100,50]]], + targeting:{}, + adType: true, + display: 'other', + bidding:{prebid:{bids:['bid1']}} + } + + const mobileAny = jest.fn().mockReturnValue(true); + global.isMobile = {any: mobileAny}; + + arcAds.registerAd(adParams); + + expect(queuePrebidCommandBindMock).toHaveBeenCalledTimes(0); + expect(displayAdBindMock).toHaveBeenCalledTimes(0); + }); + + it('handles iframeBidders case', () => { + arcAds.wrapper = { + amazon: { + enabled: true, + id: '123' + }, + prebid: { + enabled: true, + useSlotForAdUnit: true, + } + }; + const adParams = { + id: "testID", + slotname: "testSlotname", + dimensions: [[[100,50]]], + targeting:{}, + adType: true, + display: 'all', + bidding:{prebid:{bids:['bid1']}}, + iframeBidders:[], + } + + const mobileAny = jest.fn().mockReturnValue(true); + global.isMobile = {any: mobileAny}; + + arcAds.registerAd(adParams); + + expect(setConfigSpy).toHaveBeenCalledTimes(0); + }); + + it('if no processDisplayAd do not call queueGoogletagCommand' , () => { + arcAds.wrapper = { + amazon: { + enabled: true, + id: '123' + }, + prebid: { + enabled: true, + useSlotForAdUnit: true, + } + }; + const adParams = { + id: "testID", + slotname: "testSlotname", + dimensions: [[[100,50]]], + targeting:{}, + adType: true, + display: 'all', + bidding:{prebid:{bids:['bid1']}}, + iframeBidders:[], + } + arcAds.displayAd.bind = jest.fn().mockReturnValue(null); + + arcAds.registerAd(adParams); + + expect(gptService.queueGoogletagCommand).toHaveBeenCalledTimes(0); + }); + + it('if try error write console error' , () => { + arcAds.wrapper = { + amazon: { + enabled: true, + id: '123' + }, + prebid: { + enabled: true, + useSlotForAdUnit: true, + } + }; + const adParams = { + id: "testID", + slotname: "testSlotname", + dimensions: [[[100,50]]], + targeting:{}, + adType: true, + display: 'all', + bidding:{prebid:{bids:['bid1']}}, + iframeBidders:[], + } + arcAds.displayAd.bind.mockImplementation(() => { + throw new Error('test error msg'); + }); + + const errorMock = jest.fn(); + console.error = errorMock; + + arcAds.registerAd(adParams); + + expect(errorMock).toHaveBeenCalledTimes(1); + expect(errorMock.mock.calls[0][0]).toEqual('ads error'); + }); + +}); \ No newline at end of file diff --git a/src/__tests__/resources.test.js b/src/__tests__/resources.test.js new file mode 100644 index 0000000..9af4f28 --- /dev/null +++ b/src/__tests__/resources.test.js @@ -0,0 +1,48 @@ +import {appendResource} from '../util/resources.js'; + +describe('appendResource', () => { + const cbMock = jest.fn(); + const appendChildMock = jest.fn(); + const saveDocument = global.document; + + afterAll(() => { + delete global.document; + global.document = saveDocument; + }); + + beforeAll(() => { + delete global.document; + global.document = { + documentElement:{ + appendChild: appendChildMock, + }, + createElement: jest.fn().mockReturnValue({}), + } + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('if tag is script create tag and append child tag', () => { + appendResource('script', 'www.test.com', true, true, cbMock); + expect(appendChildMock).toHaveBeenCalledTimes(1); + const expectedParams = {"async": true, "defer": true, "src": "www.test.com"}; + expect(appendChildMock).toHaveBeenCalledWith(expectedParams); + expect(cbMock).toHaveBeenCalledTimes(1); + }); + + it('if tag is not script do nothing', () => { + appendResource('div', 'www.test.com', true, true, cbMock); + expect(appendChildMock).toHaveBeenCalledTimes(0); + expect(cbMock).toHaveBeenCalledTimes(0); + }); + + it('if no async or defer assume false for those values', () => { + appendResource('script', 'www.test.com', null,null , cbMock); + expect(appendChildMock).toHaveBeenCalledTimes(1); + const expectedParams = {"async": false, "defer": false, "src": "www.test.com"}; + expect(appendChildMock).toHaveBeenCalledWith(expectedParams); + expect(cbMock).toHaveBeenCalledTimes(1); + }); +}); diff --git a/src/__tests__/sizemapping.test.js b/src/__tests__/sizemapping.test.js new file mode 100644 index 0000000..b7a10b7 --- /dev/null +++ b/src/__tests__/sizemapping.test.js @@ -0,0 +1,87 @@ +import { + prepareSizeMaps, + parseSizeMappings, + runResizeEvents, + setResizeListener, + sizemapListeners, + } from '../services/sizemapping'; + import { fetchBids } from '../services/headerbidding'; + const mockSizeMap = [[468, 60], [728, 90]]; + describe('prepareSizeMaps', () => { + it('return sizeMap object', () => { + const mockDimensions = [[1000, 300], [970, 90], [728, 90], [300, 250]]; + const result = prepareSizeMaps(mockDimensions, mockSizeMap); + expect(result.mapping.length).toEqual(2); + expect(result.breakpoints.length).toEqual(2); + expect(result.correlators.length).toEqual(2); + }); + }); + describe('parseSizeMappings', () => { + Object.defineProperty(window, 'innerWidth', { + writable: true, + value: 1080, + }); + Object.defineProperty(window, 'innerHeight', { + writable: true, + value: 680, + }); + it('return sizeMap object', () => { + const result = parseSizeMappings(mockSizeMap); + expect(result).toEqual([]); + }); + }); + describe('setResizeListener', () => { + const mockParams = { + id: '123', + correlators: [1, 2] + }; + beforeEach(() => { + global.window = { + addEventListener: () => jest.fn().mockName('addEventListener'), + }; + }); + it('return sizeMap object', () => { + const mockSpy = jest.spyOn(global.window, 'addEventListener'); + setResizeListener(mockParams); + expect(mockSpy).toHaveBeenCalled(); + }); + }); + describe('runResizeEvents', () => { + beforeEach(() => { + global.runResizeEvents = { + fetchBids: () => jest.fn().mockName('fetchBids'), + refreshSlot: () => jest.fn().mockName('fetchBids'), + parseSizeMappings: () => jest.fn().mockName('parseSizeMappings'), + }; + Object.assign(sizemapListeners, { abc: { correlators: [1, 2, 3] } }); + }); + afterEach(() => { + global = {}; + }); + const mockParams = { + ad: {}, + breakpoints: [768, 1080], + id: 'abc', + bidding: { + prebid: { + enabled: true, + }, + amazon: { + enabled: false, + } + }, + mapping: mockSizeMap, + slotName: 'mockSlotName', + wrapper: {}, + prerender: false, + }; + it('set ad correlators to true', () => { + Object.defineProperty(window, 'innerWidth', { + writable: true, + value: 1024, + }); + const result = runResizeEvents(mockParams); + const resultFn = result(); + expect(sizemapListeners.abc.correlators[0]).toEqual(true); + }); + }); \ No newline at end of file diff --git a/src/__tests__/util.test.js b/src/__tests__/util.test.js index 1f829fe..eb25846 100644 --- a/src/__tests__/util.test.js +++ b/src/__tests__/util.test.js @@ -1,4 +1,5 @@ import { renamePositionKey } from '../util/customTargeting'; +import {debounce} from '../util/debounce.js'; describe('The CustomTargeting.js functions', () => { it('should take targeting and position value, and rename the key as posn', () => { @@ -17,3 +18,33 @@ describe('The CustomTargeting.js functions', () => { expect(updatedTargeting).toEqual(newTargeting); }); }); + +describe('debounce', () => { + beforeEach(() => { + jest.useFakeTimers(); + }); + + test('debounce', () => { + const func = jest.fn(); + const debouncedFunc = debounce(func, 1000); + + // Call debounced function immediately + debouncedFunc(); + expect(func).toHaveBeenCalledTimes(0); + + // Call debounced function several times with 500ms between each call + for (let i = 0; i < 10; i += 1) { + setTimeout(() => {}, 500); + debouncedFunc(); + } + + // Verify debounced function was not called yet + expect(func).toHaveBeenCalledTimes(0); + + // Fast forward time + jest.runAllTimers(); + + // Verify debounced function was only called once + expect(func).toHaveBeenCalledTimes(1); + }); +}); diff --git a/src/index.js b/src/index.js index 23c9901..2a5647c 100644 --- a/src/index.js +++ b/src/index.js @@ -17,12 +17,16 @@ export class ArcAds { this.wrapper = options.bidding || {}; this.positions = []; this.collapseEmptyDivs = options.dfp.collapseEmptyDivs; + this.adsList = []; window.isMobile = MobileDetection; if (this.dfpId === '') { - console.warn(`ArcAds: DFP id is missing from the arcads initialization script. - Documentation: https://github.com/wapopartners/arc-ads#getting-started`); + console.warn( + 'ArcAds: DFP id is missing from the arcads initialization script.', + '\n', + 'Documentation: https://github.com/wapopartners/arc-ads#getting-started' + ); } else { initializeGPT(); queueGoogletagCommand(dfpSettings.bind(this, handleSlotRendered)); @@ -101,6 +105,58 @@ export class ArcAds { }); } + /** + * @desc Registers a collection of advertisements as single prebid and ad calls + * @param {array} collection - An array containing a list of objects containing advertisement data. + **/ + registerAdCollectionSingleCall(collection, bidderTimeout = 700) { + window.blockArcAdsLoad = true; + window.blockArcAdsPrebid = true; + + collection.forEach((advert) => { + this.registerAd(advert); + }); + + window.blockArcAdsLoad = false; + window.blockArcAdsPrebid = false; + + //prebid call + pbjs.requestBids({ + timeout: bidderTimeout, + //adUnitCodes: codes, + bidsBackHandler: (result) => { + console.log('Bid Back Handler', result); + pbjs.setTargetingForGPTAsync(); + + window.googletag.pubads().refresh(window.adsList); + window.adsList = []; + } + }); + } + + + /** + * @desc Sets blockArcAdsLoad to be true - stops Ad Calls from going out, + * allowing ads to be saved up for a single ad call to be sent out later. + **/ + static setAdsBlockGate() { + const win = ArcAds.getWindow(); + if (typeof win !== 'undefined') { + win.blockArcAdsLoad = true; + } + } + + /** + * @desc Sets blockArcAdsLoad to be true - stops Ad Calls from going out, + * allowing ads to be saved up for a single ad call to be sent out later. + **/ + static releaseAdsBlockGate() { + const win = ArcAds.getWindow(); + if (typeof win !== 'undefined') { + win.blockArcAdsLoad = false; + } + } + /** * @desc Displays an advertisement and sets up any neccersary event binding. * @param {object} params - An object containing all of the function arguments. @@ -158,6 +214,10 @@ export class ArcAds { const safebreakpoints = (sizemap && sizemap.breakpoints) ? sizemap.breakpoints : []; + if (window.adsList && ad) { + adsList.push(ad); + } + if (dimensions && bidding && ((bidding.amazon && bidding.amazon.enabled) || (bidding.prebid && bidding.prebid.enabled))) { fetchBids({ ad, @@ -169,7 +229,7 @@ export class ArcAds { bidding, breakpoints: safebreakpoints }); - } else { + } else if (!window.blockArcAdsPrebid) { refreshSlot({ ad, prerender, @@ -182,4 +242,55 @@ export class ArcAds { }); } } + + /** + * @desc Send out ads that have been accumulated for the SRA + **/ + sendSingleCallAds(bidderTimeout = 700) { + // if no ads have been accumulated to send out together + // do nothing, return + if (this.adsList && this.adsList.length < 1) { + return false; + } + //ensure library is present and able to send out SRA ads + if (window && window.googletag && googletag.pubadsReady) { // eslint-disable-line + window.googletag.pubads().disableInitialLoad(); + window.googletag.pubads().enableSingleRequest(); + window.googletag.pubads().enableAsyncRendering(); + + this.registerAdCollectionSingleCall(this.adsList, bidderTimeout); + } else { + setTimeout(() => { + this.sendSingleCallAds(); + }, 2000); + } + } + + /** + * Append this ad information to the list of ads + * to be sent out as part of the singleAdCall + * + * @param {Object} params the ad parameters + */ + reserveAd(params) { + ArcAds.setAdsBlockGate(); + this.adsList.push(params); + } + + /** + * Page level targeting - any targeting set + * using this function will apply to all + * ads on the page. This is useful for SRA to + * reduce request length. + * + * @param {string} key Targeting parameter key. + * * @param {string} value Targeting parameter value or array of values. + */ + setPageLeveTargeting(key, value) { //TODO check for pubads + googletag.pubads().setTargeting(key, value); + } + + static getWindow() { + return window; + } } diff --git a/src/services/gpt.js b/src/services/gpt.js index be434ee..4a0ba61 100644 --- a/src/services/gpt.js +++ b/src/services/gpt.js @@ -44,7 +44,7 @@ export function refreshSlot({ }); function runRefreshEvent() { - if (window.blockArcAdsLoad) return; + if (window.blockArcAdsLoad) return 'blockArcAdsLoad'; if (window.googletag && googletag.pubadsReady) { window.googletag.pubads().refresh([ad], { changeCorrelator: correlator }); } else { @@ -84,6 +84,7 @@ export function dfpSettings(handleSlotRenderEnded) { window.googletag.pubads().disableInitialLoad(); window.googletag.pubads().enableSingleRequest(); window.googletag.pubads().enableAsyncRendering(); + if (this.collapseEmptyDivs) { window.googletag.pubads().collapseEmptyDivs(); } diff --git a/src/services/prebid.js b/src/services/prebid.js index afd31f6..c17c01f 100644 --- a/src/services/prebid.js +++ b/src/services/prebid.js @@ -17,11 +17,14 @@ export function queuePrebidCommand(fn) { * @param {function} prerender - An optional function that will run before the advertisement renders. * @param {function} cb - An optional callback function that should fire whenever the bidding has concluded. **/ -export function fetchPrebidBids(ad, code, timeout, info, prerender, cb = null) { - pbjs.addAdUnits(info); +export function fetchPrebidBidsArray(ad, codes, timeout, info, prerender, cb = null) { + pbjs.addAdUnits(info); //eslint-disable-line no-undef + if (window.blockArcAdsPrebid) { + return; + } pbjs.requestBids({ timeout, - adUnitCodes: [code], + adUnitCodes: codes, bidsBackHandler: (result) => { console.log('Bid Back Handler', result); pbjs.setTargetingForGPTAsync([code]); @@ -30,10 +33,16 @@ export function fetchPrebidBids(ad, code, timeout, info, prerender, cb = null) { } else { refreshSlot({ ad, info, prerender }); } - } + }, }); } +export function fetchPrebidBids(ad, code, timeout, info, prerender, cb = null) { + const newInfo = info; + newInfo.bids = Array.isArray(info.bids) ? info.bids : [info.bids]; + fetchPrebidBidsArray(ad, [code], timeout, newInfo, prerender, cb); +} + /** * @desc Registers an advertisement with Prebid.js so it's prepared to fetch bids for it. * @param {string} code - Contains the div id or slotname used for the advertisement diff --git a/src/services/sizemapping.js b/src/services/sizemapping.js index d6e31f0..5cfda22 100644 --- a/src/services/sizemapping.js +++ b/src/services/sizemapping.js @@ -19,15 +19,17 @@ export function prepareSizeMaps(dimensions, sizemap) { const correlators = []; const parsedSizemap = !sizemap.length ? null : sizemap; - parsedSizemap.forEach((value, index) => { - mapping.push([value, dimensions[index]]); - - // Filters duplicates from the mapping - if (breakpoints.indexOf(value[0]) === -1) { - breakpoints.push(value[0]); - correlators.push(false); - } - }); + if (parsedSizemap && dimensions) { + parsedSizemap.forEach((value, index) => { + mapping.push([value, dimensions[index]]); + + // Filters duplicates from the mapping + if (breakpoints.indexOf(value[0]) === -1) { + breakpoints.push(value[0]); + correlators.push(false); + } + }); + } breakpoints.sort((a, b) => { return a - b; });