From 05d0bf9f7b58c65acd31a25205e8b76b0441ebe6 Mon Sep 17 00:00:00 2001 From: chaac Date: Tue, 6 Dec 2016 10:44:54 -0800 Subject: [PATCH] Add GumGum adapter (#833) * GumGum adapter Tests * GumGum adapter - remove unneeded reset * GumGum adapter - ad loader format --- adapters.json | 1 + integrationExamples/gpt/pbjs_example_gpt.html | 6 + src/adapters/gumgum.js | 114 ++++++++++ test/spec/adapters/gumgum_spec.js | 201 ++++++++++++++++++ 4 files changed, 322 insertions(+) create mode 100644 src/adapters/gumgum.js create mode 100644 test/spec/adapters/gumgum_spec.js diff --git a/adapters.json b/adapters.json index 6fad373286b..06a353e6541 100644 --- a/adapters.json +++ b/adapters.json @@ -12,6 +12,7 @@ "conversant", "districtmDMX", "getintent", + "gumgum", "hiromedia", "indexExchange", "kruxlink", diff --git a/integrationExamples/gpt/pbjs_example_gpt.html b/integrationExamples/gpt/pbjs_example_gpt.html index 456f1ce09cb..4c5fd87798d 100644 --- a/integrationExamples/gpt/pbjs_example_gpt.html +++ b/integrationExamples/gpt/pbjs_example_gpt.html @@ -319,6 +319,12 @@ params: { tagid: 007, // REQUIRED int. To get one, contact http://www.memeglobal.com } + }, + { + bidder: 'gumgum', + params: { + inScreen: 'ggumtest' // REQUIRED str Tracking Id + } } ] } diff --git a/src/adapters/gumgum.js b/src/adapters/gumgum.js new file mode 100644 index 00000000000..36b53c4e060 --- /dev/null +++ b/src/adapters/gumgum.js @@ -0,0 +1,114 @@ +const bidfactory = require('../bidfactory'); +const bidmanager = require('../bidmanager'); +const utils = require('../utils'); +const adloader = require('../adloader'); + +const BIDDER_CODE = 'gumgum'; +const CALLBACKS = {}; + +const GumgumAdapter = function GumgumAdapter() { + + const bidEndpoint = `https://g2.gumgum.com/hbid/imp`; + + const WINDOW = global.top; + const SCREEN = WINDOW.screen; + + function _callBids({ bids }) { + const browserParams = { + vw: WINDOW.innerWidth, + vh: WINDOW.innerHeight, + sw: SCREEN.width, + sh: SCREEN.height, + pu: WINDOW.location.href, + dpr: WINDOW.devicePixelRatio || 1 + }; + utils._each(bids, bidRequest => { + const { bidId + , params = {} + , placementCode + } = bidRequest; + const trackingId = params.inScreen; + const nativeId = params.native; + const slotId = params.inSlot; + const bid = {}; + + /* slot/native ads need the placement id */ + switch (true) { + case !!(params.inImage): bid.pi = 1; break; + case !!(params.inScreen): bid.pi = 2; break; + case !!(params.inSlot): bid.pi = 3; break; + case !!(params.native): bid.pi = 5; break; + default: return utils.logWarn( + `[GumGum] No product selected for the placement ${placementCode}` + + ', please check your implementation.' + ); + } + /* tracking id is required for in-image and in-screen */ + if (trackingId) bid.t = trackingId; + /* native ads require a native placement id */ + if (nativeId) bid.ni = nativeId; + /* slot ads require a slot id */ + if (slotId) bid.si = slotId; + + const cachedBid = Object.assign({ + placementCode, + id: bidId + }, bid); + + const callback = { jsonp: `$$PREBID_GLOBAL$$.handleGumGumCB['${ bidId }']` }; + CALLBACKS[bidId] = _handleGumGumResponse(cachedBid); + const query = Object.assign(callback, browserParams, bid); + const bidCall = `${bidEndpoint}?${utils.parseQueryStringParameters(query)}`; + adloader.loadScript(bidCall); + }); + } + + const _handleGumGumResponse = cachedBidRequest => bidResponse => { + const ad = bidResponse && bidResponse.ad; + if (ad && ad.id) { + const bid = bidfactory.createBid(1); + const { t: trackingId + , pi: productId + , placementCode + } = cachedBidRequest; + bidResponse.placementCode = placementCode; + const encodedResponse = encodeURIComponent(JSON.stringify(bidResponse)); + const gumgumAdLoader = ``; + Object.assign(bid, { + cpm: ad.price, + ad: gumgumAdLoader, + width: ad.width, + height: ad.height, + bidderCode: BIDDER_CODE + }); + bidmanager.addBidResponse(cachedBidRequest.placementCode, bid); + } else { + const noBid = bidfactory.createBid(2); + noBid.bidderCode = BIDDER_CODE; + bidmanager.addBidResponse(cachedBidRequest.placementCode, noBid); + } + delete CALLBACKS[cachedBidRequest.id]; + }; + + window.$$PREBID_GLOBAL$$.handleGumGumCB = CALLBACKS; + + return { + callBids: _callBids + }; + +}; + +module.exports = GumgumAdapter; diff --git a/test/spec/adapters/gumgum_spec.js b/test/spec/adapters/gumgum_spec.js new file mode 100644 index 00000000000..1c4f8a329f7 --- /dev/null +++ b/test/spec/adapters/gumgum_spec.js @@ -0,0 +1,201 @@ +import {expect} from 'chai'; +import Adapter from '../../../src/adapters/gumgum'; +import bidManager from '../../../src/bidmanager'; +import adLoader from '../../../src/adloader'; +import { STATUS } from '../../../src/constants'; + +describe('gumgum adapter', () => { + 'use strict'; + + let adapter; + let sandbox; + + const TEST = { + PUBLISHER_IDENTITY: 'ggumtest', + BIDDER_CODE: 'gumgum', + PLACEMENT: 'placementId', + CPM: 2 + }; + const bidderRequest = { + bidderCode: TEST.BIDDER_CODE, + bids: [{ // in-screen + bidId: 'InScreenBidId', + bidder: TEST.BIDDER_CODE, + placementCode: TEST.PLACEMENT, + sizes: [ [728, 90] ], + params: { + inScreen: TEST.PUBLISHER_IDENTITY + } + }, { // in-image + bidId: 'InImageBidId', + bidder: TEST.BIDDER_CODE, + placementCode: TEST.PLACEMENT, + sizes: [ [728, 90] ], + params: { + inImage: TEST.PUBLISHER_IDENTITY + } + }, { // native + bidId: 'NativeBidId', + bidder: TEST.BIDDER_CODE, + placementCode: TEST.PLACEMENT, + sizes: [ [728, 90] ], + params: { + native: 10 + } + }, { // slot + bidId: 'InSlotBidId', + bidder: TEST.BIDDER_CODE, + placementCode: TEST.PLACEMENT, + sizes: [ [728, 90] ], + params: { + inSlot: 10 + } + }, { // no identity + bidId: 'NoIdentityBidId', + bidder: TEST.BIDDER_CODE, + placementCode: TEST.PLACEMENT, + sizes: [ [728, 90] ] + }] + }; + const bidderResponse = { + "ad": { + "id": 1, + "width": 728, + "height": 90, + "markup": "
some fancy ad
", + "ii": true, + "du": "http://example.com/", + "price": TEST.CPM, + "impurl": "http://example.com/" + } + }; + + function mockBidResponse(response) { + sandbox.stub(bidManager, 'addBidResponse'); + sandbox.stub(adLoader, 'loadScript'); + adapter.callBids(bidderRequest); + pbjs.handleGumGumCB['InScreenBidId'](response); + return bidManager.addBidResponse.firstCall.args[1]; + } + + beforeEach(() => { + adapter = new Adapter(); + sandbox = sinon.sandbox.create(); + }); + + afterEach(() => { + sandbox.restore(); + }); + + describe('callBids', () => { + + beforeEach(() => { + sandbox.stub(adLoader, 'loadScript'); + adapter.callBids(bidderRequest); + }); + + it('should call the endpoint once per valid bid', () => { + sinon.assert.callCount(adLoader.loadScript, 4); + }); + + it('should include required browser data', () => { + const endpointRequest = expect(adLoader.loadScript.firstCall.args[0]); + endpointRequest.to.include('vw'); + endpointRequest.to.include('vh'); + endpointRequest.to.include('sw'); + endpointRequest.to.include('sh'); + }); + + it('should include the publisher identity', () => { + const endpointRequest = expect(adLoader.loadScript.firstCall.args[0]); + endpointRequest.to.include('t=' + TEST.PUBLISHER_IDENTITY); + }); + + it('first call should be in-screen', () => { + expect(adLoader.loadScript.firstCall.args[0]).to.include('pi=2'); + }); + + it('second call should be in-image', () => { + expect(adLoader.loadScript.secondCall.args[0]).to.include('pi=1'); + }); + + it('third call should be native', () => { + expect(adLoader.loadScript.thirdCall.args[0]).to.include('pi=5'); + }); + + it('last call should be slot', () => { + expect(adLoader.loadScript.lastCall.args[0]).to.include('pi=3'); + }); + + }); + + describe('handleGumGumCB[...]', () => { + it('should exist and be a function', () => { + expect(pbjs.handleGumGumCB['InScreenBidId']).to.exist.and.to.be.a('function'); + }); + }); + + describe('respond with a successful bid', () => { + + let successfulBid; + + beforeEach(() => { + successfulBid = mockBidResponse(bidderResponse); + }); + + it('should add one bid', () => { + sinon.assert.calledOnce(bidManager.addBidResponse); + }); + + it('should pass the correct placement code as the first param', () => { + const [ placementCode ] = bidManager.addBidResponse.firstCall.args; + expect(placementCode).to.eql(TEST.PLACEMENT); + }); + + it('should have a GOOD status code', () => { + expect(successfulBid.getStatusCode()).to.eql(STATUS.GOOD); + }); + + it('should use the CPM returned by the server', () => { + expect(successfulBid).to.have.property('cpm', TEST.CPM); + }); + + it('should have an ad', () => { + expect(successfulBid).to.have.property('ad'); + }); + + it('should have the size specified by the server', () => { + expect(successfulBid).to.have.property('width', 728); + expect(successfulBid).to.have.property('height', 90); + }); + + }); + + describe('respond with an empty bid', () => { + + let noBid; + + beforeEach(() => { + noBid = mockBidResponse({}); + }); + + it('should add one bid', () => { + sinon.assert.calledOnce(bidManager.addBidResponse); + }); + + it('should have a NO_BID status code', () => { + expect(noBid.getStatusCode()).to.eql(STATUS.NO_BID); + }); + + it('should pass the correct placement code as the first parameter', () => { + const [ placementCode ] = bidManager.addBidResponse.firstCall.args; + expect(placementCode).to.eql(TEST.PLACEMENT); + }); + + it('should add the bidder code to the bid object', () => { + expect(noBid).to.have.property('bidderCode', TEST.BIDDER_CODE); + }); + + }); + +});