diff --git a/modules/eplanningBidAdapter.js b/modules/eplanningBidAdapter.js index ca2cbfd99..a6adab1e9 100644 --- a/modules/eplanningBidAdapter.js +++ b/modules/eplanningBidAdapter.js @@ -1,4 +1,4 @@ -import {getWindowSelf, isEmpty, parseSizesInput} from '../src/utils.js'; +import {getWindowSelf, isEmpty, parseSizesInput, isGptPubadsDefined, isSlotMatchingAdUnitCode} from '../src/utils.js'; import {getGlobal} from '../src/prebidGlobal.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {getStorageManager} from '../src/storageManager.js'; @@ -291,13 +291,25 @@ function getCharset() { function waitForElementsPresent(elements) { const observer = new MutationObserver(function (mutationList, observer) { + let index; + let adView; if (mutationList && Array.isArray(mutationList)) { mutationList.forEach(mr => { if (mr && mr.addedNodes && Array.isArray(mr.addedNodes)) { mr.addedNodes.forEach(ad => { - let index = elements.indexOf(ad.id); + index = elements.indexOf(ad.id); + adView = ad; + if (index < 0) { + elements.forEach(code => { + let div = _getAdSlotHTMLElement(code); + if (div && div.contains(ad) && div.getBoundingClientRect().width > 0) { + index = elements.indexOf(div.id); + adView = div; + } + }); + } if (index >= 0) { - registerViewability(ad); + registerViewability(adView, elements[index]); elements.splice(index, 1); if (!elements.length) { observer.disconnect(); @@ -318,19 +330,41 @@ function waitForElementsPresent(elements) { }); } -function registerViewability(div) { +function registerViewability(div, name) { visibilityHandler({ - name: div.id, + name: name, div: div }); } +function _mapAdUnitPathToElementId(adUnitCode) { + if (isGptPubadsDefined()) { + // eslint-disable-next-line no-undef + const adSlots = googletag.pubads().getSlots(); + const isMatchingAdSlot = isSlotMatchingAdUnitCode(adUnitCode); + + for (let i = 0; i < adSlots.length; i++) { + if (isMatchingAdSlot(adSlots[i])) { + const id = adSlots[i].getSlotElementId(); + return id; + } + } + } + + return null; +} + +function _getAdSlotHTMLElement(adUnitCode) { + return document.getElementById(adUnitCode) || + document.getElementById(_mapAdUnitPathToElementId(adUnitCode)); +} + function registerViewabilityAllBids(bids) { let elementsNotPresent = []; bids.forEach(bid => { - let div = document.getElementById(bid.adUnitCode); + let div = _getAdSlotHTMLElement(bid.adUnitCode); if (div) { - registerViewability(div); + registerViewability(div, bid.adUnitCode); } else { elementsNotPresent.push(bid.adUnitCode); } @@ -345,114 +379,65 @@ function getViewabilityTracker() { let VIEWABILITY_TIME = 1000; let VIEWABILITY_MIN_RATIO = 0.5; let publicApi; - let context; - - function segmentIsOutsideTheVisibleRange(visibleRangeEnd, p1, p2) { - return p1 > visibleRangeEnd || p2 < 0; - } - - function segmentBeginsBeforeTheVisibleRange(p1) { - return p1 < 0; - } - - function segmentEndsAfterTheVisibleRange(visibleRangeEnd, p2) { - return p2 < visibleRangeEnd; - } - - function axialVisibilityRatio(visibleRangeEnd, p1, p2) { - let visibilityRatio = 0; - if (!segmentIsOutsideTheVisibleRange(visibleRangeEnd, p1, p2)) { - if (segmentBeginsBeforeTheVisibleRange(p1)) { - visibilityRatio = p2 / (p2 - p1); + let observer; + let visibilityAds = {}; + + function intersectionCallback(entries) { + entries.forEach(function(entry) { + var adBox = entry.target; + if (entry.isIntersecting) { + if (entry.intersectionRatio >= VIEWABILITY_MIN_RATIO && entry.boundingClientRect && entry.boundingClientRect.height > 0 && entry.boundingClientRect.width > 0) { + visibilityAds[adBox.id] = true; + } } else { - visibilityRatio = segmentEndsAfterTheVisibleRange(visibleRangeEnd, p2) ? 1 : (visibleRangeEnd - p1) / (p2 - p1); + visibilityAds[adBox.id] = false; } - } - return visibilityRatio; - } - - function isNotHiddenByNonFriendlyIframe() { - try { return (window === window.top) || window.frameElement; } catch (e) {} - } - - function defineContext(e) { - try { - context = e && window.document.body.contains(e) ? window : (window.top.document.body.contains(e) ? top : undefined); - } catch (err) {} - return context; - } - - function getContext(e) { - return context; - } - - function verticalVisibilityRatio(position) { - return axialVisibilityRatio(getContext().innerHeight, position.top, position.bottom); - } - - function horizontalVisibilityRatio(position) { - return axialVisibilityRatio(getContext().innerWidth, position.left, position.right); - } - - function itIsNotHiddenByBannerAreaPosition(e) { - let position = e.getBoundingClientRect(); - return (verticalVisibilityRatio(position) * horizontalVisibilityRatio(position)) > VIEWABILITY_MIN_RATIO; - } - - function itIsNotHiddenByDisplayStyleCascade(e) { - return e.offsetHeight > 0 && e.offsetWidth > 0; - } - - function itIsNotHiddenByOpacityStyleCascade(e) { - let s = e.style; - let p = e.parentNode; - return !(s && parseFloat(s.opacity) === 0) && (!p || itIsNotHiddenByOpacityStyleCascade(p)); - } - - function itIsNotHiddenByVisibilityStyleCascade(e) { - return getContext().getComputedStyle(e).visibility !== 'hidden'; - } - - function itIsNotHiddenByTabFocus() { - try { return getContext().top.document.hasFocus(); } catch (e) {} - } - - function isDefined(e) { - return (e !== null) && (typeof e !== 'undefined'); + }); } - function itIsNotHiddenByOrphanBranch() { - return isDefined(getContext()); + function observedElementIsVisible(element) { + return visibilityAds[element.id] && document.visibilityState && document.visibilityState === 'visible'; } - function isContextInAnIframe() { - return isDefined(getContext().frameElement); + function defineObserver() { + if (!observer) { + var observerConfig = { + root: null, + rootMargin: '0px', + threshold: [VIEWABILITY_MIN_RATIO] + }; + observer = new IntersectionObserver(intersectionCallback.bind(this), observerConfig); + } } - function processIntervalVisibilityStatus(elapsedVisibleIntervals, element, callback) { - let visibleIntervals = isVisible(element) ? (elapsedVisibleIntervals + 1) : 0; + let visibleIntervals = observedElementIsVisible(element) ? (elapsedVisibleIntervals + 1) : 0; if (visibleIntervals === TIME_PARTITIONS) { + stopObserveViewability(element) callback(); } else { setTimeout(processIntervalVisibilityStatus.bind(this, visibleIntervals, element, callback), VIEWABILITY_TIME / TIME_PARTITIONS); } } - function isVisible(element) { - defineContext(element); - return isNotHiddenByNonFriendlyIframe() && - itIsNotHiddenByOrphanBranch() && - itIsNotHiddenByTabFocus() && - itIsNotHiddenByDisplayStyleCascade(element) && - itIsNotHiddenByVisibilityStyleCascade(element) && - itIsNotHiddenByOpacityStyleCascade(element) && - itIsNotHiddenByBannerAreaPosition(element) && - (!isContextInAnIframe() || isVisible(getContext().frameElement)); + function stopObserveViewability(element) { + delete visibilityAds[element.id]; + observer.unobserve(element); + } + + function observeAds(element) { + observer.observe(element); + } + + function initAndVerifyVisibility(element, callback) { + if (element) { + defineObserver(); + observeAds(element); + processIntervalVisibilityStatus(0, element, callback); + } } publicApi = { - isVisible: isVisible, - onView: processIntervalVisibilityStatus.bind(this, 0) + onView: initAndVerifyVisibility.bind(this) }; return publicApi; diff --git a/test/spec/modules/eplanningBidAdapter_spec.js b/test/spec/modules/eplanningBidAdapter_spec.js index 52d2fb761..db70f7956 100644 --- a/test/spec/modules/eplanningBidAdapter_spec.js +++ b/test/spec/modules/eplanningBidAdapter_spec.js @@ -6,6 +6,7 @@ import {init, getIds} from 'modules/userId/index.js'; import * as utils from 'src/utils.js'; import {hook} from '../../../src/hook.js'; import {getGlobal} from '../../../src/prebidGlobal.js'; +import { makeSlot } from '../integration/faker/googletag.js'; describe('E-Planning Adapter', function () { const adapter = newBidder('spec'); @@ -649,7 +650,24 @@ describe('E-Planning Adapter', function () { let element; let getBoundingClientRectStub; let sandbox = sinon.sandbox.create(); - let focusStub; + let intersectionObserverStub; + let intersectionCallback; + + function setIntersectionObserverMock(params) { + let fakeIntersectionObserver = (stateChange, options) => { + intersectionCallback = stateChange; + return { + unobserve: (element) => { + return element; + }, + observe: (element) => { + intersectionCallback([{'target': {'id': element.id}, 'isIntersecting': params[element.id].isIntersecting, 'intersectionRatio': params[element.id].ratio, 'boundingClientRect': {'width': params[element.id].width, 'height': params[element.id].height}}]); + }, + }; + }; + + intersectionObserverStub = sandbox.stub(window, 'IntersectionObserver').callsFake(fakeIntersectionObserver); + } function createElement(id) { element = document.createElement('div'); element.id = id || ADUNIT_CODE_VIEW; @@ -741,9 +759,6 @@ describe('E-Planning Adapter', function () { hasLocalStorageStub.returns(true); clock = sandbox.useFakeTimers(); - - focusStub = sandbox.stub(window.top.document, 'hasFocus'); - focusStub.returns(true); }); afterEach(function () { $$PREBID_GLOBAL$$.bidderSettings = {}; @@ -786,6 +801,7 @@ describe('E-Planning Adapter', function () { let respuesta; beforeEach(function () { createElementVisible(); + setIntersectionObserverMock({[ADUNIT_CODE_VIEW]: {'ratio': 1, 'isIntersecting': true, 'width': 200, 'height': 200}}); }); it('when you have a render', function() { respuesta = spec.buildRequests(bidRequests, bidderRequest); @@ -823,6 +839,7 @@ describe('E-Planning Adapter', function () { let respuesta; beforeEach(function () { createElementOutOfView(); + setIntersectionObserverMock({[ADUNIT_CODE_VIEW]: {'ratio': 0, 'isIntersecting': false, 'width': 200, 'height': 200}}); }); it('when you have a render', function() { @@ -848,6 +865,7 @@ describe('E-Planning Adapter', function () { let respuesta; it('should register visibility with more than 50%', function() { createPartiallyVisibleElement(); + setIntersectionObserverMock({[ADUNIT_CODE_VIEW]: {'ratio': 0.6, 'isIntersecting': true, 'width': 200, 'height': 200}}); respuesta = spec.buildRequests(bidRequests, bidderRequest); clock.tick(1005); @@ -856,6 +874,7 @@ describe('E-Planning Adapter', function () { }); it('you should not register visibility with less than 50%', function() { createPartiallyInvisibleElement(); + setIntersectionObserverMock({[ADUNIT_CODE_VIEW]: {'ratio': 0.4, 'isIntersecting': true, 'width': 200, 'height': 200}}); respuesta = spec.buildRequests(bidRequests, bidderRequest); clock.tick(1005); @@ -863,12 +882,29 @@ describe('E-Planning Adapter', function () { expect(storage.getDataFromLocalStorage(storageIdView)).to.equal(null); }); }); + context('when element id is not equal to adunitcode', function() { + let respuesta; + it('should register visibility with more than 50%', function() { + const code = ADUNIT_CODE_VIEW; + const divId = 'div-gpt-ad-123'; + createPartiallyVisibleElement(divId); + window.googletag.pubads().setSlots([makeSlot({ code, divId })]); + setIntersectionObserverMock({[divId]: {'ratio': 0.6, 'isIntersecting': true, 'width': 200, 'height': 200}}); + + respuesta = spec.buildRequests(bidRequests, bidderRequest); + clock.tick(1005); + + expect(storage.getDataFromLocalStorage(storageIdRender)).to.equal('1'); + expect(storage.getDataFromLocalStorage(storageIdView)).to.equal('1'); + }); + }); context('when width or height of the element is zero', function() { beforeEach(function () { createElementVisible(); }); it('if the width is zero but the height is within the range', function() { element.style.width = '0px'; + setIntersectionObserverMock({[ADUNIT_CODE_VIEW]: {'ratio': 0.4, 'isIntersecting': true, 'width': 200, 'height': 200}}); spec.buildRequests(bidRequests, bidderRequest) clock.tick(1005); @@ -877,6 +913,7 @@ describe('E-Planning Adapter', function () { }); it('if the height is zero but the width is within the range', function() { element.style.height = '0px'; + setIntersectionObserverMock({[ADUNIT_CODE_VIEW]: {'ratio': 1, 'isIntersecting': true, 'width': 500, 'height': 0}}); spec.buildRequests(bidRequests, bidderRequest) clock.tick(1005); @@ -886,6 +923,7 @@ describe('E-Planning Adapter', function () { it('if both are zero', function() { element.style.height = '0px'; element.style.width = '0px'; + setIntersectionObserverMock({[ADUNIT_CODE_VIEW]: {'ratio': 1, 'isIntersecting': true, 'width': 0, 'height': 0}}); spec.buildRequests(bidRequests, bidderRequest) clock.tick(1005); @@ -893,16 +931,6 @@ describe('E-Planning Adapter', function () { expect(storage.getDataFromLocalStorage(storageIdView)).to.equal(null); }); }); - context('when tab is inactive', function() { - it('I should not register if it is not in focus', function() { - createElementVisible(); - focusStub.returns(false); - spec.buildRequests(bidRequests, bidderRequest); - clock.tick(1005); - expect(storage.getDataFromLocalStorage(storageIdRender)).to.equal('1'); - expect(storage.getDataFromLocalStorage(storageIdView)).to.equal(null); - }); - }); context('segmentBeginsBeforeTheVisibleRange', function() { it('segmentBeginsBeforeTheVisibleRange', function() { createElementOutOfRange(); @@ -933,7 +961,11 @@ describe('E-Planning Adapter', function () { createElementVisible(ADUNIT_CODE_VIEW); createElementVisible(ADUNIT_CODE_VIEW2); createElementVisible(ADUNIT_CODE_VIEW3); - + setIntersectionObserverMock({ + [ADUNIT_CODE_VIEW]: {'ratio': 1, 'isIntersecting': true, 'width': 200, 'height': 200}, + [ADUNIT_CODE_VIEW2]: {'ratio': 1, 'isIntersecting': true, 'width': 200, 'height': 200}, + [ADUNIT_CODE_VIEW3]: {'ratio': 1, 'isIntersecting': true, 'width': 200, 'height': 200} + }); respuesta = spec.buildRequests(bidRequestMultiple, bidderRequest); clock.tick(1005); [ADUNIT_CODE_VIEW, ADUNIT_CODE_VIEW2, ADUNIT_CODE_VIEW3].forEach(ac => { @@ -946,7 +978,11 @@ describe('E-Planning Adapter', function () { createElementOutOfView(ADUNIT_CODE_VIEW); createElementOutOfView(ADUNIT_CODE_VIEW2); createElementOutOfView(ADUNIT_CODE_VIEW3); - + setIntersectionObserverMock({ + [ADUNIT_CODE_VIEW]: {'ratio': 0, 'isIntersecting': false, 'width': 200, 'height': 200}, + [ADUNIT_CODE_VIEW2]: {'ratio': 0, 'isIntersecting': false, 'width': 200, 'height': 200}, + [ADUNIT_CODE_VIEW3]: {'ratio': 0, 'isIntersecting': false, 'width': 200, 'height': 200} + }); respuesta = spec.buildRequests(bidRequestMultiple, bidderRequest); clock.tick(1005); [ADUNIT_CODE_VIEW, ADUNIT_CODE_VIEW2, ADUNIT_CODE_VIEW3].forEach(ac => { @@ -960,7 +996,11 @@ describe('E-Planning Adapter', function () { createElementVisible(ADUNIT_CODE_VIEW); createElementOutOfView(ADUNIT_CODE_VIEW2); createElementOutOfView(ADUNIT_CODE_VIEW3); - + setIntersectionObserverMock({ + [ADUNIT_CODE_VIEW]: {'ratio': 1, 'isIntersecting': true, 'width': 200, 'height': 200}, + [ADUNIT_CODE_VIEW2]: {'ratio': 0.3, 'isIntersecting': true, 'width': 200, 'height': 200}, + [ADUNIT_CODE_VIEW3]: {'ratio': 0, 'isIntersecting': false, 'width': 200, 'height': 200} + }); respuesta = spec.buildRequests(bidRequestMultiple, bidderRequest); clock.tick(1005); expect(storage.getDataFromLocalStorage('pbsr_' + ADUNIT_CODE_VIEW)).to.equal('6'); @@ -989,7 +1029,7 @@ describe('E-Planning Adapter', function () { sandbox.restore(); }) - it('should add eids to the request', function() { + it('should add eids to the request ', function() { let bidRequests = [validBidView]; const expected_id5id = encodeURIComponent(JSON.stringify({ uid: 'ID5-ZHMOL_IfFSt7_lVYX8rBZc6GH3XMWyPQOBUfr4bm0g!', ext: { linkType: 1 } })); const request = spec.buildRequests(bidRequests, bidderRequest);