diff --git a/modules/yieldmoSyntheticInventoryModule.js b/modules/yieldmoSyntheticInventoryModule.js index 8412713dda6..cda90c2f578 100644 --- a/modules/yieldmoSyntheticInventoryModule.js +++ b/modules/yieldmoSyntheticInventoryModule.js @@ -1,10 +1,10 @@ import { config } from '../src/config.js'; import { isGptPubadsDefined, isFn } from '../src/utils.js'; +import * as ajax from '../src/ajax.js' import strIncludes from 'core-js-pure/features/string/includes.js'; export const MODULE_NAME = 'Yieldmo Synthetic Inventory Module'; export const AD_SERVER_ENDPOINT = 'https://ads.yieldmo.com/v002/t_ads/ads'; -export const AD_REQUEST_TYPE = 'GET'; const USPAPI_VERSION = 1; let cmpVersion = 0; @@ -23,7 +23,22 @@ export function init(config) { window.top.googletag = window.top.googletag || {}; window.top.googletag.cmd = window.top.googletag.cmd || []; } - getAd(`${AD_SERVER_ENDPOINT}?${serialize(collectData(config.placementId, consentDataObj))}`, config); + ajax.ajaxBuilder()(`${AD_SERVER_ENDPOINT}?${serialize(collectData(config.placementId, consentDataObj))}`, { + success: (responceText, responseObj) => { + window.top.__ymAds = processResponse(responseObj); + const googletag = window.top.googletag; + googletag.cmd.push(() => { + if (window.top.document.body) { + googletagCmd(config, googletag); + } else { + window.top.document.addEventListener('DOMContentLoaded', () => googletagCmd(config, googletag)); + } + }); + }, + error: (message, err) => { + throw err; + } + }); } } }; @@ -58,11 +73,6 @@ function collectData(placementId, consentDataObj) { const connection = window.navigator.connection || {}; const description = Array.prototype.slice.call(document.getElementsByTagName('meta')) .filter((meta) => meta.getAttribute('name') === 'description')[0]; - const pageDimensions = { - density: window.top.devicePixelRatio || 0, - height: window.top.screen.height || window.top.screen.availHeight || window.top.outerHeight || window.top.innerHeight || 481, - width: window.top.screen.width || window.top.screen.availWidth || window.top.outerWidth || window.top.innerWidth || 321, - }; return { bust: timeStamp, @@ -74,15 +84,15 @@ function collectData(placementId, consentDataObj) { p: placementId, description: description ? description.content.substring(0, 1000) : '', title: document.title, - scrd: pageDimensions.density, - h: pageDimensions.height, - w: pageDimensions.width, + scrd: window.top.devicePixelRatio || 0, + h: window.top.screen.height || window.top.screen.availHeight || window.top.outerHeight || window.top.innerHeight || 481, + w: window.top.screen.width || window.top.screen.availWidth || window.top.outerWidth || window.top.innerWidth || 321, pft: timeStamp, ct: timeStamp, - connect: connection.effectiveType, - bwe: connection.downlink ? connection.downlink + 'Mb/sec' : '', - rtt: connection.rtt, - sd: connection.saveData, + connect: typeof connection.effectiveType !== 'undefined' ? connection.effectiveType : undefined, + bwe: typeof connection.downlink !== 'undefined' ? connection.downlink + 'Mb/sec' : undefined, + rtt: typeof connection.rtt !== 'undefined' ? String(connection.rtt) : undefined, + sd: typeof connection.saveData !== 'undefined' ? String(connection.saveData) : undefined, us_privacy: (consentDataObj.usp && consentDataObj.usp.usPrivacy) || '', cmp: (consentDataObj.cmp && consentDataObj.cmp.tcString) || '' }; @@ -98,35 +108,20 @@ function serialize(dataObj) { return str.join('&'); } -function processResponse(response) { - let responseBody; +function processResponse(res) { + let parsedResponseBody; try { - responseBody = JSON.parse(response.responseText); + parsedResponseBody = JSON.parse(res.responseText); } catch (err) { - throw new Error(`${MODULE_NAME}: response body is not valid JSON`); + throw new Error(`${MODULE_NAME}: response is not valid JSON`); } - if (response.status !== 200 || !responseBody.data || !responseBody.data.length || !responseBody.data[0].ads || !responseBody.data[0].ads.length) { - throw new Error(`${MODULE_NAME}: NOAD`); + if (res && res.status === 204) { + throw new Error(`${MODULE_NAME}: no content success status`); } - return responseBody; -} - -function getAd(url, config) { - const req = new XMLHttpRequest(); - req.open(AD_REQUEST_TYPE, url, true); - req.onload = (e) => { - const response = processResponse(e.target); - window.top.__ymAds = response; - const googletag = window.top.googletag; - googletag.cmd.push(() => { - if (window.top.document.body) { - googletagCmd(config, googletag); - } else { - window.top.document.addEventListener('DOMContentLoaded', () => googletagCmd(config, googletag)); - } - }); - }; - req.send(null); + if (parsedResponseBody.data && parsedResponseBody.data.length && parsedResponseBody.data[0].error_code) { + throw new Error(`${MODULE_NAME}: no ad, error_code: ${parsedResponseBody.data[0].error_code}`); + } + return parsedResponseBody; } function checkSandbox(w) { diff --git a/test/spec/modules/yieldmoSyntheticInventoryModule_spec.js b/test/spec/modules/yieldmoSyntheticInventoryModule_spec.js index 120f896c078..e04f9f01388 100644 --- a/test/spec/modules/yieldmoSyntheticInventoryModule_spec.js +++ b/test/spec/modules/yieldmoSyntheticInventoryModule_spec.js @@ -1,4 +1,5 @@ import { expect } from 'chai'; +import * as ajax from 'src/ajax.js'; import { init, MODULE_NAME, @@ -68,16 +69,15 @@ describe('Yieldmo Synthetic Inventory Module', function() { }).throw(`${MODULE_NAME}: adUnitPath required`); }); - describe('getAd', () => { - let requestMock = { - open: sinon.stub(), - send: sinon.stub(), - }; - const originalXMLHttpRequest = window.XMLHttpRequest; - const originalConnection = window.navigator.connection; - let clock; - let adServerRequest; - let response; + describe('Ajax ad request', () => { + let sandbox; + + const setAjaxStub = (cb) => { + const ajaxStub = sandbox.stub().callsFake(cb); + sandbox.stub(ajax, 'ajaxBuilder').callsFake(() => ajaxStub); + return ajaxStub; + } + const responseData = { data: [{ ads: [{ @@ -87,118 +87,92 @@ describe('Yieldmo Synthetic Inventory Module', function() { }; beforeEach(() => { - window.XMLHttpRequest = function FakeXMLHttpRequest() { - this.open = requestMock.open; - this.send = requestMock.send; - - adServerRequest = this; - }; - - response = { - target: { - responseText: JSON.stringify(responseData), - status: 200, - } - }; - - clock = sinon.useFakeTimers(); - Object.defineProperty(window.navigator, 'connection', { value: {}, writable: true }); + sandbox = sinon.sandbox.create(); }); afterEach(() => { - window.XMLHttpRequest = originalXMLHttpRequest; - - requestMock.open.resetBehavior(); - requestMock.open.resetHistory(); - requestMock.send.resetBehavior(); - requestMock.send.resetHistory(); - - adServerRequest = undefined; - - clock.restore(); - }); - - after(() => { - window.navigator.connection = originalConnection; + sandbox.restore(); }); it('should open ad request to ad server', () => { + const ajaxStub = setAjaxStub((url, callbackObj) => {}); + init(mockedYmConfig); - const adServerHost = (new URL(requestMock.open.getCall(0).args[1])).host; - expect(adServerHost).to.be.equal('ads.yieldmo.com'); + expect((new URL(ajaxStub.getCall(0).args[0])).host).to.be.equal('ads.yieldmo.com'); }); it('should properly combine ad request query', () => { - const pageDimensions = { - density: window.top.devicePixelRatio || 0, - height: window.top.screen.height || window.screen.top.availHeight || window.top.outerHeight || window.top.innerHeight || 481, - width: window.top.screen.width || window.screen.top.availWidth || window.top.outerWidth || window.top.innerWidth || 321, - }; + const title = 'Test title value'; + const ajaxStub = setAjaxStub((url, callbackObj) => {}); + const documentStubTitle = sandbox.stub(document, 'title').value(title); + const connection = window.navigator.connection || {}; init(mockedYmConfig); - - const queryParams = getQuearyParamsFromUrl(requestMock.open.getCall(0).args[1]); - + const queryParams = getQuearyParamsFromUrl(ajaxStub.getCall(0).args[0]); const timeStamp = queryParams.bust; - expect(queryParams).to.deep.equal({ + const paramsToCompare = { + title, _s: '1', dnt: 'false', e: '4', - h: `${pageDimensions.height}`, p: mockedYmConfig.placementId, page_url: window.top.location.href, pr: window.top.location.href, - scrd: `${pageDimensions.density}`, - w: `${pageDimensions.width}`, - title: document.title, - }); - }); - - it('should send ad request to ad server', () => { - init(mockedYmConfig); + bust: timeStamp, + pft: timeStamp, + ct: timeStamp, + connect: typeof connection.effectiveType !== 'undefined' ? connection.effectiveType : undefined, + bwe: typeof connection.downlink !== 'undefined' ? connection.downlink + 'Mb/sec' : undefined, + rtt: typeof connection.rtt !== 'undefined' ? String(connection.rtt) : undefined, + sd: typeof connection.saveData !== 'undefined' ? String(connection.saveData) : undefined, + scrd: String(window.top.devicePixelRatio || 0), + h: String(window.top.screen.height || window.screen.top.availHeight || window.top.outerHeight || window.top.innerHeight || 481), + w: String(window.top.screen.width || window.screen.top.availWidth || window.top.outerWidth || window.top.innerWidth || 321), + }; - expect(requestMock.send.calledOnceWith(null)).to.be.true; + expect(queryParams).to.eql(JSON.parse(JSON.stringify(paramsToCompare))); }); - it('should throw an error if can not parse response', () => { - response.target.responseText = undefined; + it('should send ad request to ad server', () => { + const ajaxStub = setAjaxStub((url, callbackObj) => {}); init(mockedYmConfig); - expect(() => adServerRequest.onload(response)).to.throw(); + expect(ajaxStub.calledOnce).to.be.true; }); - it('should throw an error if status is not 200', () => { - response.target.status = 500; - - init(mockedYmConfig); + it('should throw an error if can not parse response', () => { + const ajaxStub = setAjaxStub((url, callbackObj) => { + callbackObj.success('', {responseText: '__invalid_JSON__', status: 200}); + }); - expect(() => adServerRequest.onload(response)).to.throw(); + expect(() => init(mockedYmConfig)).to.throw('Yieldmo Synthetic Inventory Module: response is not valid JSON'); }); - it('should throw an error if there is no data in response', () => { - response.target.responseText = '{}'; - - init(mockedYmConfig); + it('should throw an error if status is 204', () => { + const ajaxStub = setAjaxStub((url, callbackObj) => { + callbackObj.success('', {status: 204, responseText: '{}'}); + }); - expect(() => adServerRequest.onload(response)).to.throw(); + expect(() => init(mockedYmConfig)).to.throw('Yieldmo Synthetic Inventory Module: no content success status'); }); - it('should throw an error if there is no ads in response data', () => { - response.target.responseText = '{ data: [{}] }'; - - init(mockedYmConfig); + it('should throw an error if error_code present in the ad response', () => { + const ajaxStub = setAjaxStub((url, callbackObj) => { + callbackObj.success('', {status: 200, responseText: '{"data": [{"error_code": "NOAD"}]}'}); + }); - expect(() => adServerRequest.onload(response)).to.throw(); + expect(() => init(mockedYmConfig)).to.throw('Yieldmo Synthetic Inventory Module: no ad, error_code: NOAD'); }); it('should store ad response in window object', () => { - init(mockedYmConfig); - - adServerRequest.onload(response); + const ajaxStub = setAjaxStub((url, callbackObj) => { + callbackObj.success(JSON.stringify(responseData), {status: 200, responseText: JSON.stringify(responseData)}); + }); + init(mockedYmConfig); expect(window.top.__ymAds).to.deep.equal(responseData); }); @@ -206,9 +180,11 @@ describe('Yieldmo Synthetic Inventory Module', function() { const containerName = 'ym_sim_container_' + mockedYmConfig.placementId; const gtag = setGoogletag(); - init(mockedYmConfig); + const ajaxStub = setAjaxStub((url, callbackObj) => { + callbackObj.success(JSON.stringify(responseData), {status: 200, responseText: '{"data": [{"ads": []}]}'}); + }); - adServerRequest.onload(response); + init(mockedYmConfig); expect(gtag.cmd.length).to.equal(1); @@ -252,6 +228,7 @@ describe('Yieldmo Synthetic Inventory Module', function() { window.XMLHttpRequest = function FakeXMLHttpRequest() { this.open = requestMock.open; this.send = requestMock.send; + this.setRequestHeader = () => {}; }; clock = sinon.useFakeTimers(); @@ -426,6 +403,7 @@ describe('Yieldmo Synthetic Inventory Module', function() { window.XMLHttpRequest = function FakeXMLHttpRequest() { this.open = requestMock.open; this.send = requestMock.send; + this.setRequestHeader = () => {}; }; clock = sinon.useFakeTimers();