diff --git a/adapters.json b/adapters.json index a49141c8345..60f53d9a074 100644 --- a/adapters.json +++ b/adapters.json @@ -18,6 +18,7 @@ "indexExchange", "kruxlink", "komoona", + "mantis", "openx", "piximedia", "pubmatic", diff --git a/src/adapters/mantis.js b/src/adapters/mantis.js new file mode 100644 index 00000000000..5b4b17f1638 --- /dev/null +++ b/src/adapters/mantis.js @@ -0,0 +1,223 @@ +var bidfactory = require('../bidfactory.js'); +var bidmanager = require('../bidmanager.js'); +var adloader = require('../adloader'); +var constants = require('../constants.json'); + +module.exports = function () { + function inIframe() { + try { + return window.self !== window.top && !window.mantis_link; + } catch (e) { + return true; + } + } + + function isDesktop(ignoreTouch) { + var scope = function (win) { + var width = win.innerWidth || win.document.documentElement.clientWidth || win.document.body.clientWidth; + var supportsTouch = !ignoreTouch && ('ontouchstart' in window || navigator.msMaxTouchPoints); + + return !supportsTouch && (!width || width >= (window.mantis_breakpoint || 768)); + }; + + if (inIframe()) { + try { + return scope(window.top); + } catch (ex) { + } + } + + return scope(window); + } + + function isSendable(val) { + if (val === null || val === undefined) { + return false; + } + + if (typeof val === 'string') { + return !(!val || /^\s*$/.test(val)); + } + + if (typeof val === 'number') { + return !isNaN(val); + } + + return true; + } + + function isObject(value) { + return Object.prototype.toString.call(value) === '[object Object]'; + } + + function isAmp() { + return typeof window.context === "object" && (window.context.tagName === "AMP-AD" || window.context.tagName === "AMP-EMBED"); + } + + function isSecure() { + return document.location.protocol === "https:"; + } + + function isArray(value) { + return Object.prototype.toString.call(value) === '[object Array]'; + } + + function jsonp(callback) { + if (!window.mantis_jsonp) { + window.mantis_jsonp = []; + } + + window.mantis_jsonp.push(callback); + + return 'mantis_jsonp[' + (window.mantis_jsonp.length - 1) + ']'; + } + + function jsonToQuery(data, chain, form) { + if (!data) { + return null; + } + + var parts = form || []; + + for (var key in data) { + var queryKey = key; + + if (chain) { + queryKey = chain + '[' + key + ']'; + } + + var val = data[key]; + + if (isArray(val)) { + for (var index = 0; index < val.length; index++) { + var akey = queryKey + '[' + index + ']'; + var aval = val[index]; + + if (isObject(aval)) { + jsonToQuery(aval, akey, parts); + } else if (isSendable(aval)) { + parts.push(akey + '=' + encodeURIComponent(aval)); + } + } + } else if (isObject(val)) { + jsonToQuery(val, queryKey, parts); + } else if (isSendable(val)) { + parts.push(queryKey + '=' + encodeURIComponent(val)); + } + } + + return parts.join('&'); + } + + + function buildMantisUrl(path, data, domain) { + var params = { + referrer: document.referrer, + tz: new Date().getTimezoneOffset(), + buster: new Date().getTime(), + secure: isSecure() + }; + + if (!inIframe() || isAmp()) { + params.mobile = !isAmp() && isDesktop(true) ? 'false' : 'true'; + } + + if (window.mantis_uuid) { + params.uuid = window.mantis_uuid; + } else if (window.localStorage) { + var localUuid = window.localStorage.getItem('mantis:uuid'); + + if (localUuid) { + params.uuid = localUuid; + } + } + + if (!inIframe()) { + try { + params.title = window.top.document.title; + params.referrer = window.top.document.referrer; + params.url = window.top.document.location.href; + } catch (ex) { + + } + } else { + params.iframe = true; + } + + if (isAmp()) { + if (!params.url && window.context.canonicalUrl) { + params.url = window.context.canonicalUrl; + } + + if (!params.url && window.context.location) { + params.url = window.context.location.href; + } + + if (!params.referrer && window.context.referrer) { + params.referrer = window.context.referrer; + } + } + + Object.keys(data || {}).forEach(function (key) { + params[key] = data[key]; + }); + + var query = jsonToQuery(params); + + return (window.mantis_domain === undefined ? domain || 'https://mantodea.mantisadnetwork.com' : window.mantis_domain) + path + '?' + query; + } + + var Prebid = function (bidfactory, bidmanager, adloader, constants) { + return { + callBids: function (params) { + var property = null; + + params.bids.some(function (bid) { + if (bid.params.property) { + property = bid.params.property; + + return true; + } + }); + + var url = { + jsonp: jsonp(function (resp) { + params.bids.forEach(function (bid) { + var ad = resp.ads[bid.bidId]; + + var bidObject; + + if (ad) { + bidObject = bidfactory.createBid(constants.STATUS.GOOD); + bidObject.bidderCode = 'mantis'; + bidObject.cpm = ad.cpm; + bidObject.ad = ad.html; + bidObject.width = ad.width; + bidObject.height = ad.height; + } else { + bidObject = bidfactory.createBid(constants.STATUS.NO_BID); + bidObject.bidderCode = 'mantis'; + } + + bidmanager.addBidResponse(bid.placementCode, bidObject); + }); + }), + property: property, + bids: params.bids.map(function (bid) { + return { + bidId: bid.bidId, + sizes: bid.sizes.map(function (size) { + return {width: size[0], height: size[1]}; + }) + }; + }), + version: 1 + }; + + adloader.loadScript(buildMantisUrl('/website/prebid', url)); + } + }; + }; + + return new Prebid(bidfactory, bidmanager, adloader, constants); +}; \ No newline at end of file diff --git a/test/spec/adapters/mantis_spec.js b/test/spec/adapters/mantis_spec.js new file mode 100644 index 00000000000..094baad8239 --- /dev/null +++ b/test/spec/adapters/mantis_spec.js @@ -0,0 +1,166 @@ +'use strict'; + +describe('mantis adapter tests', function () { + const expect = require('chai').expect; + const adapter = require('src/adapters/mantis.js'); + const bidmanager = require('src/bidmanager'); + const adloader = require('src/adloader'); + const constants = require('src/constants.json'); + + var mantis, sandbox; + + beforeEach(() => { + mantis = new adapter(); + sandbox = sinon.sandbox.create(); + }); + + + afterEach(() => { + sandbox.restore(); + + delete window.context; + delete window.mantis_link; + delete window.mantis_breakpoint; + delete window.mantis_uuid; + }); + + var callBidExample = { + bidderCode: 'mantis', + bids: [ + { + bidId: 'bidId1', + bidder: 'mantis', + placementCode: 'foo', + sizes: [[728, 90]], + params: { + property: '1234' + } + }, + { + bidId: 'bidId2', + bidder: 'mantis', + placementCode: 'bar', + sizes: [[300, 600], [300, 250]], + params: { + property: '1234' + } + } + ] + }; + + describe('callBids', () => { + it('should create appropriate bid responses', () => { + sandbox.stub(bidmanager, 'addBidResponse'); + sandbox.stub(adloader, 'loadScript', function (url) { + var jsonp = eval(decodeURIComponent(url.match(/jsonp=(.*)&property/)[1])); + + jsonp({ + ads: { + bidId1: { + cpm: 1, + html: '', + width: 300, + height: 600 + } + } + }); + }); + + mantis.callBids(callBidExample); + + sinon.assert.calledTwice(bidmanager.addBidResponse); + + expect(bidmanager.addBidResponse.firstCall.args[0]).to.eql('foo'); + + var bid1 = bidmanager.addBidResponse.firstCall.args[1]; + expect(bid1.getStatusCode()).to.eql(constants.STATUS.GOOD); + expect(bid1.bidderCode).to.eql('mantis'); + expect(bid1.cpm).to.eql(1); + expect(bid1.ad).to.eql(''); + expect(bid1.width).to.eql(300); + expect(bid1.height).to.eql(600); + + expect(bidmanager.addBidResponse.secondCall.args[0]).to.eql('bar'); + + var bid2 = bidmanager.addBidResponse.secondCall.args[1]; + expect(bid2.getStatusCode()).to.eql(constants.STATUS.NO_BID); + expect(bid2.bidderCode).to.eql('mantis'); + }); + + it('should load script with relevant bid data', () => { + sandbox.stub(adloader, 'loadScript'); + + mantis.callBids(callBidExample); + + sinon.assert.calledOnce(adloader.loadScript); + + var serverCall = adloader.loadScript.firstCall.args[0]; + + expect(serverCall).to.match(/buster=[0-9]+&/); + expect(serverCall).to.match(/tz=[0-9]+&/); + expect(serverCall).to.match(/secure=(true|false)&/); + expect(serverCall).to.string('property=1234&'); + expect(serverCall).to.string('bids[0][bidId]=bidId1&'); + expect(serverCall).to.string('bids[0][sizes][0][width]=728&'); + expect(serverCall).to.string('bids[0][sizes][0][height]=90&'); + expect(serverCall).to.string('bids[1][bidId]=bidId2&'); + expect(serverCall).to.string('bids[1][sizes][0][width]=300&'); + expect(serverCall).to.string('bids[1][sizes][0][height]=600&'); + expect(serverCall).to.string('bids[1][sizes][1][width]=300&'); + expect(serverCall).to.string('bids[1][sizes][1][height]=250&'); + expect(serverCall).to.string('version=1'); + }); + + /* tests below are to just adhere to code coverage requirements, but it is already tested in our own libraries/deployment process */ + it('should send uuid from window if set', () => { + sandbox.stub(adloader, 'loadScript'); + + window.mantis_uuid = '4321'; + + mantis.callBids(callBidExample); + + sinon.assert.calledOnce(adloader.loadScript); + + var serverCall = adloader.loadScript.firstCall.args[0]; + + expect(serverCall).to.string('uuid=4321&'); + }); + + it('should send mobile = true if breakpoint is hit', () => { + sandbox.stub(adloader, 'loadScript'); + + window.mantis_link = true; // causes iframe detection to not work + window.mantis_breakpoint = 100000000; // force everything to be mobile + + mantis.callBids(callBidExample); + + sinon.assert.calledOnce(adloader.loadScript); + + var serverCall = adloader.loadScript.firstCall.args[0]; + + expect(serverCall).to.string('mobile=true&'); + }); + + it('should send different params if amp is detected', () => { + sandbox.stub(adloader, 'loadScript'); + + window.context = { + tagName: "AMP-AD", + location: { + href: 'bar', + referrer: 'baz' + } + }; + + mantis.callBids(callBidExample); + + sinon.assert.calledOnce(adloader.loadScript); + + var serverCall = adloader.loadScript.firstCall.args[0]; + + expect(serverCall).to.string('mobile=true&'); + expect(serverCall).to.string('url=bar&'); + }); + }); +}); +