Skip to content

Commit

Permalink
Merge pull request #14 from andrepolischuk/viewable-impression-2s
Browse files Browse the repository at this point in the history
fix: fix viewable impression by vast spec
  • Loading branch information
andrepolischuk authored Apr 6, 2021
2 parents a08a70e + 60497df commit 441350d
Show file tree
Hide file tree
Showing 2 changed files with 90 additions and 47 deletions.
97 changes: 53 additions & 44 deletions src/adUnit/VideoAdUnit.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,15 @@
import {linearEvents} from '../tracker';
import {getViewable} from '../vastSelectors';
import {finish} from './adUnitEvents';
import {
onElementVisibilityChange,
onElementResize
} from './helpers/dom/elementObservers';
import {onElementVisibilityChange, onElementResize} from './helpers/dom/elementObservers';
import preventManualProgress from './helpers/dom/preventManualProgress';
import Emitter from './helpers/Emitter';
import retrieveIcons from './helpers/icons/retrieveIcons';
import addIcons from './helpers/icons/addIcons';
import viewmode from './helpers/vpaid/viewmode';
import safeCallback from './helpers/safeCallback';

const {
start,
viewable,
viewUndetermined,
iconClick,
iconView
} = linearEvents;
const {start, viewable, notViewable, viewUndetermined, iconClick, iconView} = linearEvents;

// eslint-disable-next-line id-match
export const _protected = Symbol('_protected');
Expand All @@ -43,6 +34,14 @@ class VideoAdUnit extends Emitter {
});
},
finished: false,
handleViewableImpression: (event) => {
this[_protected].viewable = event;

this.emit(event, {
adUnit: this,
type: event
});
},
onErrorCallbacks: [],
onFinishCallbacks: [],
started: false,
Expand All @@ -58,7 +57,7 @@ class VideoAdUnit extends Emitter {
};

/** Ad unit type */
type=null;
type = null;

/** If an error occurs it will contain the reference to the error otherwise it will be bull */
error = null;
Expand All @@ -82,9 +81,7 @@ class VideoAdUnit extends Emitter {
constructor (vastChain, videoAdContainer, {viewability = false, responsive = false, logger = console} = {}) {
super(logger);

const {
onFinishCallbacks
} = this[_protected];
const {onFinishCallbacks, handleViewableImpression} = this[_protected];

/** Reference to the {@link VastChain} used to load the ad. */
this.vastChain = vastChain;
Expand All @@ -98,22 +95,20 @@ class VideoAdUnit extends Emitter {
onFinishCallbacks.push(preventManualProgress(this.videoAdContainer.videoElement));

if (this.icons) {
const {
drawIcons,
hasPendingIconRedraws,
removeIcons
} = addIcons(this.icons, {
const {drawIcons, hasPendingIconRedraws, removeIcons} = addIcons(this.icons, {
logger,
onIconClick: (icon) => this.emit(iconClick, {
adUnit: this,
data: icon,
type: iconClick
}),
onIconView: (icon) => this.emit(iconView, {
adUnit: this,
data: icon,
type: iconView
}),
onIconClick: (icon) =>
this.emit(iconClick, {
adUnit: this,
data: icon,
type: iconClick
}),
onIconView: (icon) =>
this.emit(iconView, {
adUnit: this,
data: icon,
type: iconView
}),
videoAdContainer
});

Expand All @@ -128,24 +123,38 @@ class VideoAdUnit extends Emitter {

if (viewableImpression) {
this.once(start, () => {
const unsubscribe = onElementVisibilityChange(this.videoAdContainer.element, (visible) => {
if (this.isFinished()) {
return;
}
let timeoutId;

if (visible !== false && !this[_protected].viewable) {
const status = visible ? viewable : viewUndetermined;
const unsubscribe = onElementVisibilityChange(
this.videoAdContainer.element,
(visible) => {
if (this.isFinished() || this[_protected].viewable) {
return;
}

this.emit(status, {
adUnit: this,
type: status
});
if (typeof visible !== 'boolean') {
handleViewableImpression(viewUndetermined);

this[_protected].viewable = status;
}
}, {viewabilityOffset: 0.5});
return;
}

onFinishCallbacks.push(unsubscribe);
if (visible) {
timeoutId = setTimeout(handleViewableImpression, 2000, viewable);
} else {
clearTimeout(timeoutId);
}
},
{viewabilityOffset: 0.5}
);

onFinishCallbacks.push(() => {
unsubscribe();
clearTimeout(timeoutId);

if (!this[_protected].viewable) {
handleViewableImpression(notViewable);
}
});
});
}

Expand Down
40 changes: 37 additions & 3 deletions src/adUnit/__tests__/VideoAdUnit.spec.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/* eslint-disable no-loop-func, max-nested-callbacks */
import {vpaidInlineAd, vpaidInlineParsedXML, vastVpaidInlineXML} from '../../../fixtures';
import VideoAdContainer from '../../adContainer/VideoAdContainer';
import {iconClick, iconView, start, viewable, viewUndetermined} from '../../tracker/linearEvents';
import {iconClick, iconView, notViewable, start, viewable, viewUndetermined} from '../../tracker/linearEvents';
import {getViewable} from '../../vastSelectors';
import addIcons from '../helpers/icons/addIcons';
import retrieveIcons from '../helpers/icons/retrieveIcons';
Expand Down Expand Up @@ -510,7 +510,7 @@ describe('VideoAdUnit', () => {
});
});

test('must emit viewable on visibility change if true', () => {
test('must emit viewable when the ad unit meets criteria for a viewable impression', async () => {
const adUnit = new VideoAdUnit(vpaidChain, videoAdContainer);

jest.spyOn(adUnit, 'emit');
Expand All @@ -519,13 +519,26 @@ describe('VideoAdUnit', () => {
expect(onElementVisibilityChange).toHaveBeenCalledTimes(1);

expect(adUnit.emit).not.toHaveBeenCalledWith(viewable);
expect(adUnit.emit).not.toHaveBeenCalledWith(notViewable);
expect(adUnit.emit).not.toHaveBeenCalledWith(viewUndetermined);

simulateVisibilityChange(false);
expect(adUnit.emit).not.toHaveBeenCalledWith(viewable);
expect(adUnit.emit).not.toHaveBeenCalledWith(notViewable);
expect(adUnit.emit).not.toHaveBeenCalledWith(viewUndetermined);

const eventPromise = new Promise((resolve) => adUnit.on(viewable, resolve));

simulateVisibilityChange(true);

await eventPromise;

expect(adUnit.emit).toHaveBeenCalledWith(viewable, expect.any(Object));
expect(adUnit.emit).not.toHaveBeenCalledWith(notViewable);
expect(adUnit.emit).not.toHaveBeenCalledWith(viewUndetermined);
});

test('must emit viewUndetermined if visibility is not determined', () => {
test('must emit viewUndetermined if cannot be determined whether the ad unit meets criteria for a viewable impression', () => {
const adUnit = new VideoAdUnit(vpaidChain, videoAdContainer);

jest.spyOn(adUnit, 'emit');
Expand All @@ -534,12 +547,33 @@ describe('VideoAdUnit', () => {
expect(onElementVisibilityChange).toHaveBeenCalledTimes(1);

expect(adUnit.emit).not.toHaveBeenCalledWith(viewable);
expect(adUnit.emit).not.toHaveBeenCalledWith(notViewable);
expect(adUnit.emit).not.toHaveBeenCalledWith(viewUndetermined);

simulateVisibilityChange(undefined);
expect(adUnit.emit).not.toHaveBeenCalledWith(viewable);
expect(adUnit.emit).not.toHaveBeenCalledWith(notViewable);
expect(adUnit.emit).toHaveBeenCalledWith(viewUndetermined, expect.any(Object));
});

test('must emit notViewable on finish if ad unit never meets criteria for a viewable impression', () => {
const adUnit = new VideoAdUnit(vpaidChain, videoAdContainer);

jest.spyOn(adUnit, 'emit');
expect(onElementVisibilityChange).not.toHaveBeenCalled();
adUnit.emit(start);
expect(onElementVisibilityChange).toHaveBeenCalledTimes(1);

expect(adUnit.emit).not.toHaveBeenCalledWith(viewable);
expect(adUnit.emit).not.toHaveBeenCalledWith(notViewable);
expect(adUnit.emit).not.toHaveBeenCalledWith(viewUndetermined);

adUnit[_protected].finish();
expect(adUnit.emit).not.toHaveBeenCalledWith(viewable);
expect(adUnit.emit).not.toHaveBeenCalledWith(viewUndetermined);
expect(adUnit.emit).toHaveBeenCalledWith(notViewable, expect.any(Object));
});

test('must do nothing if finished', () => {
const adUnit = new VideoAdUnit(vpaidChain, videoAdContainer);

Expand Down

0 comments on commit 441350d

Please sign in to comment.