Skip to content

Commit

Permalink
add intersection RtdProvider and spec (prebid#7710)
Browse files Browse the repository at this point in the history
Co-authored-by: atkachov <atkachov@admixer.ua>
  • Loading branch information
2 people authored and Chris Pabst committed Jan 10, 2022
1 parent 2aebb1d commit d1e6a4f
Show file tree
Hide file tree
Showing 2 changed files with 255 additions and 0 deletions.
114 changes: 114 additions & 0 deletions modules/intersectionRtdProvider.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import {submodule} from '../src/hook.js';
import {isFn, logError} from '../src/utils.js';
import {config} from '../src/config.js';
import {getGlobal} from '../src/prebidGlobal.js';
import includes from 'core-js-pure/features/array/includes.js';
import '../src/adapterManager.js';
let observerAvailable = true;
function getIntersectionData(requestBidsObject, onDone, providerConfig, userConsent) {
const intersectionMap = {};
const placeholdersMap = {};
let done = false;
if (!observerAvailable) return complete();
const observer = new IntersectionObserver(observerCallback, {threshold: 0.5});
const adUnitCodes = requestBidsObject.adUnitCodes || [];
const auctionDelay = config.getConfig('realTimeData.auctionDelay') || 0;
const waitForIt = providerConfig.waitForIt;
let adUnits = requestBidsObject.adUnits || getGlobal().adUnits || [];
if (adUnitCodes.length) {
adUnits = adUnits.filter(unit => includes(adUnitCodes, unit.code));
}
let checkTimeoutId;
findAndObservePlaceholders();
if (auctionDelay > 0) {
setTimeout(complete, auctionDelay);
}
function findAndObservePlaceholders() {
const observed = adUnits.filter((unit) => {
const code = unit.code;
if (placeholdersMap[code]) return true;
const ph = document.getElementById(code);
if (ph) {
placeholdersMap[code] = ph;
observer.observe(ph);
return true;
}
});
if (
observed.length === adUnits.length ||
!waitForIt ||
auctionDelay <= 0
) {
return;
}
checkTimeoutId = setTimeout(findAndObservePlaceholders);
}
function observerCallback(entries) {
let entry = entries.pop();
while (entry) {
const target = entry.target;
const id = target.getAttribute('id');
if (id) {
const intersection = intersectionMap[id];
if (!intersection || intersection.time < entry.time) {
intersectionMap[id] = {
'boundingClientRect': cloneRect(entry.boundingClientRect),
'intersectionRect': cloneRect(entry.intersectionRect),
'rootRect': cloneRect(entry.rootRect),
'intersectionRatio': entry.intersectionRatio,
'isIntersecting': entry.isIntersecting,
'time': entry.time
};
if (adUnits.every(unit => !!intersectionMap[unit.code])) {
complete();
}
}
}
entry = entries.pop();
}
}
function complete() {
if (done) return;
if (checkTimeoutId) clearTimeout(checkTimeoutId);
done = true;
checkTimeoutId = null;
observer && observer.disconnect();
adUnits && adUnits.forEach((unit) => {
const intersection = intersectionMap[unit.code];
if (intersection && unit.bids) {
unit.bids.forEach(bid => bid.intersection = intersection);
}
});
onDone();
}
}
function init(moduleConfig) {
if (!isFn(window.IntersectionObserver)) {
logError('IntersectionObserver is not defined');
observerAvailable = false;
} else {
observerAvailable = true;
}
return observerAvailable;
}
function cloneRect(rect) {
return rect ? {
'left': rect.left,
'top': rect.top,
'right': rect.right,
'bottom': rect.bottom,
'width': rect.width,
'height': rect.height,
'x': rect.x,
'y': rect.y,
} : rect;
}
export const intersectionSubmodule = {
name: 'intersection',
getBidRequestData: getIntersectionData,
init: init,
};
function registerSubModule() {
submodule('realTimeData', intersectionSubmodule);
}
registerSubModule();
141 changes: 141 additions & 0 deletions test/spec/modules/intersectionRtdProvider_spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
import {config as _config, config} from 'src/config.js';
import { expect } from 'chai';
import events from 'src/events.js';
import * as prebidGlobal from 'src/prebidGlobal.js';
import { intersectionSubmodule } from 'modules/intersectionRtdProvider.js';
import * as utils from 'src/utils.js';
import {getGlobal} from 'src/prebidGlobal.js';
import 'src/prebid.js';

describe('Intersection RTD Provider', function () {
let sandbox;
let placeholder;
const pbjs = getGlobal();
const adUnit = {
code: 'ad-slot-1',
mediaTypes: {
banner: {
sizes: [ [300, 250] ]
}
},
bids: [
{
bidder: 'fake'
}
]
};
const providerConfig = {name: 'intersection', waitForIt: true};
const rtdConfig = {realTimeData: {auctionDelay: 200, dataProviders: [providerConfig]}}
describe('IntersectionObserver not supported', function() {
beforeEach(function() {
sandbox = sinon.sandbox.create();
});
afterEach(function() {
sandbox.restore();
sandbox = undefined;
});
it('init should return false', function () {
sandbox.stub(window, 'IntersectionObserver').value(undefined);
expect(intersectionSubmodule.init({})).is.false;
});
});
describe('IntersectionObserver supported', function() {
beforeEach(function() {
sandbox = sinon.sandbox.create();
placeholder = createDiv();
append();
const __config = {};
sandbox.stub(_config, 'getConfig').callsFake(function (path) {
return utils.deepAccess(__config, path);
});
sandbox.stub(_config, 'setConfig').callsFake(function (obj) {
utils.mergeDeep(__config, obj);
});
});
afterEach(function() {
sandbox.restore();
remove();
sandbox = undefined;
placeholder = undefined;
pbjs.removeAdUnit();
});
it('init should return true', function () {
expect(intersectionSubmodule.init({})).is.true;
});
it('should set intersection. (request with "adUnitCodes")', function(done) {
pbjs.addAdUnits([utils.deepClone(adUnit)]);
config.setConfig(rtdConfig);
const onDone = sandbox.stub();
const requestBidObject = {adUnitCodes: [adUnit.code]};
intersectionSubmodule.init({});
intersectionSubmodule.getBidRequestData(
requestBidObject,
onDone,
providerConfig
);
setTimeout(function() {
expect(pbjs.adUnits[0].bids[0]).to.have.property('intersection');
done();
}, 200);
});
it('should set intersection. (request with "adUnits")', function(done) {
config.setConfig(rtdConfig);
const onDone = sandbox.stub();
const requestBidObject = {adUnits: [utils.deepClone(adUnit)]};
intersectionSubmodule.init();
intersectionSubmodule.getBidRequestData(
requestBidObject,
onDone,
providerConfig
);
setTimeout(function() {
expect(requestBidObject.adUnits[0].bids[0]).to.have.property('intersection');
done();
}, 200);
});
it('should set intersection. (request all)', function(done) {
pbjs.addAdUnits([utils.deepClone(adUnit)]);
config.setConfig(rtdConfig);
const onDone = sandbox.stub();
const requestBidObject = {};
intersectionSubmodule.init({});
intersectionSubmodule.getBidRequestData(
requestBidObject,
onDone,
providerConfig
);
setTimeout(function() {
expect(pbjs.adUnits[0].bids[0]).to.have.property('intersection');
done();
}, 200);
});
it('should call done due timeout', function(done) {
config.setConfig(rtdConfig);
remove();
const onDone = sandbox.stub();
const requestBidObject = {adUnits: [utils.deepClone(adUnit)]};
intersectionSubmodule.init({});
intersectionSubmodule.getBidRequestData(
requestBidObject,
onDone,
{...providerConfig, test: 1}
);
setTimeout(function() {
sinon.assert.calledOnce(onDone);
expect(requestBidObject.adUnits[0].bids[0]).to.not.have.property('intersection');
done();
}, 300);
});
});
function createDiv() {
const div = document.createElement('div');
div.id = adUnit.code;
return div;
}
function append() {
placeholder && document.body.appendChild(placeholder);
}
function remove() {
placeholder && placeholder.parentElement && placeholder.parentElement.removeChild(placeholder);
}
});

0 comments on commit d1e6a4f

Please sign in to comment.