Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Intersection RTD Module: add new RTD module #7710

Merged
merged 2 commits into from
Nov 17, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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);
}
});