UNPKG

mk9-prebid

Version:

Header Bidding Management Library

752 lines (655 loc) 23.2 kB
import { fetchTargetingForMediaId, getVatFromCache, extractPublisherParams, formatTargetingResponse, getVatFromPlayer, enrichAdUnits, addTargetingToBid, fetchTargetingInformation, jwplayerSubmodule } from 'modules/jwplayerRtdProvider.js'; import { server } from 'test/mocks/xhr.js'; describe('jwplayerRtdProvider', function() { const testIdForSuccess = 'test_id_for_success'; const testIdForFailure = 'test_id_for_failure'; const validSegments = ['test_seg_1', 'test_seg_2']; const responseHeader = {'Content-Type': 'application/json'}; describe('Fetch targeting for mediaID tests', function () { let request; describe('Fetch succeeds', function () { beforeEach(function () { fetchTargetingForMediaId(testIdForSuccess); request = server.requests[0]; }); afterEach(function () { server.respond(); }); it('should reach out to media endpoint', function () { expect(request.url).to.be.eq(`https://cdn.jwplayer.com/v2/media/${testIdForSuccess}`); }); it('should write to cache when successful', function () { request.respond( 200, responseHeader, JSON.stringify({ playlist: [ { file: 'test.mp4', jwpseg: validSegments } ] }) ); const targetingInfo = getVatFromCache(testIdForSuccess); const validTargeting = { segments: validSegments, mediaID: testIdForSuccess }; expect(targetingInfo).to.deep.equal(validTargeting); }); }); describe('Fetch fails', function () { beforeEach(function () { fetchTargetingForMediaId(testIdForFailure); request = server.requests[0] }); it('should not write to cache when response is malformed', function() { request.respond('{]'); const targetingInfo = getVatFromCache(testIdForFailure); expect(targetingInfo).to.be.null; }); it('should not write to cache when playlist is absent', function() { request.respond({}); const targetingInfo = getVatFromCache(testIdForFailure); expect(targetingInfo).to.be.null; }); it('should not write to cache when segments are absent', function() { request.respond( 200, responseHeader, JSON.stringify({ playlist: [ { file: 'test.mp4' } ] }) ); const targetingInfo = getVatFromCache(testIdForFailure); expect(targetingInfo).to.be.null; }); it('should not write to cache when request errors', function() { request.error(); const targetingInfo = getVatFromCache(testIdForFailure); expect(targetingInfo).to.be.null; }); }); }); describe('Format targeting response', function () { it('should exclude segment key when absent', function () { const targeting = formatTargetingResponse({ mediaID: 'test' }); expect(targeting).to.not.have.property('segments'); }); it('should exclude content block when mediaId is absent', function () { const targeting = formatTargetingResponse({ segments: ['test'] }); expect(targeting).to.not.have.property('content'); }); it('should return proper format', function () { const segments = ['123']; const mediaID = 'test'; const expectedContentId = 'jw_' + mediaID; const expectedContent = { id: expectedContentId }; const targeting = formatTargetingResponse({ segments, mediaID }); expect(targeting).to.have.deep.property('segments', segments); expect(targeting).to.have.deep.property('content', expectedContent); }); }); describe('Get VAT from player', function () { const mediaIdWithSegment = 'media_ID_1'; const mediaIdNoSegment = 'media_ID_2'; const mediaIdForCurrentItem = 'media_ID_current'; const mediaIdNotCached = 'media_test_ID'; const validPlayerID = 'player_test_ID_valid'; const invalidPlayerID = 'player_test_ID_invalid'; it('returns null when jwplayer.js is absent from page', function () { const targeting = getVatFromPlayer(invalidPlayerID, mediaIdNotCached); expect(targeting).to.be.null; }); describe('When jwplayer.js is on page', function () { const playlistItemWithSegmentMock = { mediaid: mediaIdWithSegment, jwpseg: validSegments }; const targetingForMediaWithSegment = { segments: validSegments, mediaID: mediaIdWithSegment }; const playlistItemNoSegmentMock = { mediaid: mediaIdNoSegment }; const currentItemSegments = ['test_seg_3', 'test_seg_4']; const currentPlaylistItemMock = { mediaid: mediaIdForCurrentItem, jwpseg: currentItemSegments }; const targetingForCurrentItem = { segments: currentItemSegments, mediaID: mediaIdForCurrentItem }; const playerInstanceMock = { getPlaylist: function () { return [playlistItemWithSegmentMock, playlistItemNoSegmentMock]; }, getPlaylistItem: function () { return currentPlaylistItemMock; } }; const jwplayerMock = function(playerID) { if (playerID === validPlayerID) { return playerInstanceMock; } else { return {}; } }; beforeEach(function () { window.jwplayer = jwplayerMock; }); it('returns null when player ID does not match player on page', function () { const targeting = getVatFromPlayer(invalidPlayerID, mediaIdNotCached); expect(targeting).to.be.null; }); it('returns segments when media ID matches a playlist item with segments', function () { const targeting = getVatFromPlayer(validPlayerID, mediaIdWithSegment); expect(targeting).to.deep.equal(targetingForMediaWithSegment); }); it('caches segments when media ID matches a playist item with segments', function () { getVatFromPlayer(validPlayerID, mediaIdWithSegment); const vat = getVatFromCache(mediaIdWithSegment); expect(vat.segments).to.deep.equal(validSegments); }); it('returns segments of current item when media ID is missing', function () { const targeting = getVatFromPlayer(validPlayerID); expect(targeting).to.deep.equal(targetingForCurrentItem); }); it('caches segments from the current item', function () { getVatFromPlayer(validPlayerID); window.jwplayer = null; const targeting = getVatFromCache(mediaIdForCurrentItem); expect(targeting).to.deep.equal(targetingForCurrentItem); }); it('returns undefined segments when segments are absent', function () { const targeting = getVatFromPlayer(validPlayerID, mediaIdNoSegment); expect(targeting).to.deep.equal({ mediaID: mediaIdNoSegment, segments: undefined }); }); describe('Get Bid Request Data', function () { it('executes immediately while request is active if player has item', function () { const bidRequestSpy = sinon.spy(); const fakeServer = sinon.createFakeServer(); fakeServer.respondImmediately = false; fakeServer.autoRespond = false; fetchTargetingForMediaId(mediaIdWithSegment); const bid = {}; const adUnit = { ortb2Imp: { ext: { data: { jwTargeting: { mediaID: mediaIdWithSegment, playerID: validPlayerID } } } }, bids: [ bid ] }; const expectedContentId = 'jw_' + mediaIdWithSegment; const expectedTargeting = { segments: validSegments, content: { id: expectedContentId } }; jwplayerSubmodule.getBidRequestData({ adUnits: [adUnit] }, bidRequestSpy); expect(bidRequestSpy.calledOnce).to.be.true; expect(bid.rtd.jwplayer).to.have.deep.property('targeting', expectedTargeting); fakeServer.respond(); expect(bidRequestSpy.calledOnce).to.be.true; }); }); }); }); describe('Enrich ad units', function () { const contentIdForSuccess = 'jw_' + testIdForSuccess; const expectedTargetingForSuccess = { segments: validSegments, content: { id: contentIdForSuccess } }; let bidRequestSpy; let fakeServer; let clock; beforeEach(function () { bidRequestSpy = sinon.spy(); fakeServer = sinon.createFakeServer(); fakeServer.respondImmediately = false; fakeServer.autoRespond = false; clock = sinon.useFakeTimers(); }); afterEach(function () { clock.restore(); fakeServer.respond(); }); it('adds targeting when pending request succeeds', function () { fetchTargetingForMediaId(testIdForSuccess); const bids = [ { id: 'bid1' }, { id: 'bid2' } ]; const adUnit = { ortb2Imp: { ext: { data: { jwTargeting: { mediaID: testIdForSuccess } } } }, bids }; enrichAdUnits([adUnit]); const bid1 = bids[0]; const bid2 = bids[1]; expect(bid1).to.not.have.property('rtd'); expect(bid2).to.not.have.property('rtd'); const request = fakeServer.requests[0]; request.respond( 200, responseHeader, JSON.stringify({ playlist: [ { file: 'test.mp4', jwpseg: validSegments } ] }) ); expect(bid1.rtd.jwplayer).to.have.deep.property('targeting', expectedTargetingForSuccess); expect(bid2.rtd.jwplayer).to.have.deep.property('targeting', expectedTargetingForSuccess); }); it('immediately adds cached targeting', function () { fetchTargetingForMediaId(testIdForSuccess); const bids = [ { id: 'bid1' }, { id: 'bid2' } ]; const adUnit = { ortb2Imp: { ext: { data: { jwTargeting: { mediaID: testIdForSuccess } } } }, bids }; const request = fakeServer.requests[0]; request.respond( 200, responseHeader, JSON.stringify({ playlist: [ { file: 'test.mp4', jwpseg: validSegments } ] }) ); enrichAdUnits([adUnit]); const bid1 = bids[0]; const bid2 = bids[1]; expect(bid1.rtd.jwplayer).to.have.deep.property('targeting', expectedTargetingForSuccess); expect(bid2.rtd.jwplayer).to.have.deep.property('targeting', expectedTargetingForSuccess); }); it('adds content block when segments are absent and no request is pending', function () { const expectedTargetingForFailure = { content: { id: 'jw_' + testIdForFailure } }; const bids = [ { id: 'bid1' }, { id: 'bid2' } ]; const adUnit = { ortb2Imp: { ext: { data: { jwTargeting: { mediaID: testIdForFailure } } } }, bids }; enrichAdUnits([adUnit]); const bid1 = bids[0]; const bid2 = bids[1]; expect(bid1.rtd.jwplayer).to.have.deep.property('targeting', expectedTargetingForFailure); expect(bid2.rtd.jwplayer).to.have.deep.property('targeting', expectedTargetingForFailure); }); }); describe(' Extract Publisher Params', function () { const config = { mediaID: 'test' }; it('should exclude adUnits that do not support instream video and do not specify jwTargeting', function () { const oustreamAdUnit = { mediaTypes: { video: { context: 'outstream' } } }; const oustreamTargeting = extractPublisherParams(oustreamAdUnit, config); expect(oustreamTargeting).to.be.undefined; const bannerAdUnit = { mediaTypes: { banner: {} } }; const bannerTargeting = extractPublisherParams(bannerAdUnit, config); expect(bannerTargeting).to.be.undefined; const targeting = extractPublisherParams({}, config); expect(targeting).to.be.undefined; }); it('should include ad unit when media type is video and is instream', function () { const adUnit = { mediaTypes: { video: { context: 'instream' } } }; const targeting = extractPublisherParams(adUnit, config); expect(targeting).to.deep.equal(config); }); it('should include banner ad units that specify jwTargeting', function() { const adUnit = { mediaTypes: { banner: {} }, ortb2Imp: { ext: { data: { jwTargeting: {} } } } }; const targeting = extractPublisherParams(adUnit, config); expect(targeting).to.deep.equal(config); }); it('should include outstream ad units that specify jwTargeting', function() { const adUnit = { mediaTypes: { video: { context: 'outstream' } }, ortb2Imp: { ext: { data: { jwTargeting: {} } } } }; const targeting = extractPublisherParams(adUnit, config); expect(targeting).to.deep.equal(config); }); it('should fallback to config when empty jwTargeting is defined in ad unit', function () { const adUnit = { ortb2Imp: { ext: { data: { jwTargeting: {} } } } }; const targeting = extractPublisherParams(adUnit, config); expect(targeting).to.deep.equal(config); }); it('should prioritize adUnit properties ', function () { const expectedMediaID = 'test_media_id'; const expectedPlayerID = 'test_player_id'; const config = { playerID: 'bad_id', mediaID: 'bad_id' }; const adUnit = { ortb2Imp: { ext: { data: { jwTargeting: { mediaID: expectedMediaID, playerID: expectedPlayerID } } } } }; const targeting = extractPublisherParams(adUnit, config); expect(targeting).to.have.property('mediaID', expectedMediaID); expect(targeting).to.have.property('playerID', expectedPlayerID); }); it('should use config properties as fallbacks', function () { const expectedMediaID = 'test_media_id'; const expectedPlayerID = 'test_player_id'; const config = { playerID: expectedPlayerID, mediaID: 'bad_id' }; const adUnit = { ortb2Imp: { ext: { data: { jwTargeting: { mediaID: expectedMediaID } } } } }; const targeting = extractPublisherParams(adUnit, config); expect(targeting).to.have.property('mediaID', expectedMediaID); expect(targeting).to.have.property('playerID', expectedPlayerID); }); it('should return undefined when Publisher Params are absent', function () { const targeting = extractPublisherParams({}, null); expect(targeting).to.be.undefined; }) }); describe('Add Targeting to Bid', function () { const targeting = {foo: 'bar'}; it('creates realTimeData when absent from Bid', function () { const targeting = {foo: 'bar'}; const bid = {}; addTargetingToBid(bid, targeting); expect(bid).to.have.property('rtd'); expect(bid).to.have.nested.property('rtd.jwplayer.targeting', targeting); }); it('adds to existing realTimeData', function () { const otherRtd = { targeting: { seg: 'rtd seg' } }; const bid = { rtd: { otherRtd } }; addTargetingToBid(bid, targeting); expect(bid).to.have.property('rtd'); const rtd = bid.rtd; expect(rtd).to.have.property('jwplayer'); expect(rtd).to.have.nested.property('jwplayer.targeting', targeting); expect(rtd).to.have.deep.property('otherRtd', otherRtd); }); it('adds to existing realTimeData.jwplayer', function () { const otherInfo = { seg: 'rtd seg' }; const bid = { rtd: { jwplayer: { otherInfo } } }; addTargetingToBid(bid, targeting); expect(bid).to.have.property('rtd'); const rtd = bid.rtd; expect(rtd).to.have.property('jwplayer'); expect(rtd).to.have.nested.property('jwplayer.otherInfo', otherInfo); expect(rtd).to.have.nested.property('jwplayer.targeting', targeting); }); it('overrides existing jwplayer.targeting', function () { const otherInfo = { seg: 'rtd seg' }; const bid = { rtd: { jwplayer: { targeting: { otherInfo } } } }; addTargetingToBid(bid, targeting); expect(bid).to.have.property('rtd'); const rtd = bid.rtd; expect(rtd).to.have.property('jwplayer'); expect(rtd).to.have.nested.property('jwplayer.targeting', targeting); }); it('creates jwplayer when absent from realTimeData', function () { const bid = { rtd: {} }; addTargetingToBid(bid, targeting); expect(bid).to.have.property('rtd'); const rtd = bid.rtd; expect(rtd).to.have.property('jwplayer'); expect(rtd).to.have.nested.property('jwplayer.targeting', targeting); }); }); describe('jwplayerSubmodule', function () { it('successfully instantiates', function () { expect(jwplayerSubmodule.init()).to.equal(true); }); describe('Get Bid Request Data', function () { const validMediaIDs = ['media_ID_1', 'media_ID_2', 'media_ID_3']; let bidRequestSpy; let fakeServer; let clock; let bidReqConfig; beforeEach(function () { bidReqConfig = { adUnits: [ { ortb2Imp: { ext: { data: { jwTargeting: { mediaID: validMediaIDs[0] } } } }, bids: [ {}, {} ] }, { ortb2Imp: { ext: { data: { jwTargeting: { mediaID: validMediaIDs[1] } } } }, bids: [ {}, {} ] } ] }; bidRequestSpy = sinon.spy(); fakeServer = sinon.createFakeServer(); fakeServer.respondImmediately = false; fakeServer.autoRespond = false; clock = sinon.useFakeTimers(); }); afterEach(function () { clock.restore(); fakeServer.respond(); }); it('executes callback immediately when ad units are missing', function () { jwplayerSubmodule.getBidRequestData({ adUnits: [] }, bidRequestSpy); expect(bidRequestSpy.calledOnce).to.be.true; }); it('executes callback immediately when no requests are pending', function () { fetchTargetingInformation({ mediaIDs: [] }); jwplayerSubmodule.getBidRequestData(bidReqConfig, bidRequestSpy); expect(bidRequestSpy.calledOnce).to.be.true; }); it('executes callback only after requests in adUnit complete', function() { fetchTargetingInformation({ mediaIDs: validMediaIDs }); jwplayerSubmodule.getBidRequestData(bidReqConfig, bidRequestSpy); expect(bidRequestSpy.notCalled).to.be.true; const req1 = fakeServer.requests[0]; const req2 = fakeServer.requests[1]; const req3 = fakeServer.requests[2]; req1.respond(); expect(bidRequestSpy.notCalled).to.be.true; req2.respond(); expect(bidRequestSpy.calledOnce).to.be.true; req3.respond(); expect(bidRequestSpy.calledOnce).to.be.true; }); it('sets targeting data in proper structure', function () { const bid = {}; const adUnitWithMediaId = { ortb2Imp: { ext: { data: { jwTargeting: { mediaID: testIdForSuccess } } } }, bids: [ bid ] }; const adUnitEmpty = { code: 'test_ad_unit_empty' }; const expectedContentId = 'jw_' + testIdForSuccess; const expectedTargeting = { segments: validSegments, content: { id: expectedContentId } }; jwplayerSubmodule.getBidRequestData({ adUnits: [adUnitWithMediaId, adUnitEmpty] }, bidRequestSpy); expect(bidRequestSpy.calledOnce).to.be.true; expect(bid.rtd.jwplayer).to.have.deep.property('targeting', expectedTargeting); }); it('excludes segments when absent', function () { const adUnitCode = 'test_ad_unit'; const bid = {}; const adUnit = { ortb2Imp: { ext: { data: { jwTargeting: { mediaID: testIdForFailure } } } }, bids: [ bid ] }; const expectedContentId = 'jw_' + adUnit.ortb2Imp.ext.data.jwTargeting.mediaID; const expectedTargeting = { content: { id: expectedContentId } }; jwplayerSubmodule.getBidRequestData({ adUnits: [ adUnit ] }, bidRequestSpy); expect(bidRequestSpy.calledOnce).to.be.true; expect(bid.rtd.jwplayer.targeting).to.not.have.property('segments'); expect(bid.rtd.jwplayer.targeting).to.not.have.property('segments'); expect(bid.rtd.jwplayer).to.have.deep.property('targeting', expectedTargeting); }); it('does not modify bid when jwTargeting block is absent', function () { const adUnitCode = 'test_ad_unit'; const bid1 = {}; const bid2 = {}; const bid3 = {}; const adUnitWithMediaId = { code: adUnitCode, mediaID: testIdForSuccess, bids: [ bid1 ] }; const adUnitEmpty = { code: 'test_ad_unit_empty', bids: [ bid2 ] }; const adUnitEmptyfpd = { code: 'test_ad_unit_empty_fpd', ortb2Imp: { ext: { id: 'sthg' } }, bids: [ bid3 ] }; jwplayerSubmodule.getBidRequestData({ adUnits: [adUnitWithMediaId, adUnitEmpty, adUnitEmptyfpd] }, bidRequestSpy); expect(bidRequestSpy.calledOnce).to.be.true; expect(bid1).to.not.have.property('rtd'); expect(bid2).to.not.have.property('rtd'); expect(bid3).to.not.have.property('rtd'); }); }); }); });