UNPKG

video-ad-sdk

Version:

VAST/VPAID SDK that allows video ads to be played on top of any player

1,247 lines (1,028 loc) 35.7 kB
import { vpaidInlineAd, vpaidInlineParsedXML, vastVpaidInlineXML } from '../../../fixtures' import {VideoAdContainer} from '../../adContainer/VideoAdContainer' import {loadCreative} from '../helpers/vpaid/loadCreative' import {handshake} from '../helpers/vpaid/handshake' import {initAd} from '../helpers/vpaid/initAd' import {callAndWait} from '../helpers/vpaid/callAndWait' import {AdUnitError} from '../helpers/adUnitError' import {VpaidAdUnit} from '../VpaidAdUnit' import { adLoaded, adStarted, adStopped, adPlaying, resumeAd, adPaused, pauseAd, skipAd, resizeAd, adSizeChange, adVideoComplete, adError, EVENTS, adSkipped, adVolumeChange, adImpression, adVideoStart, adVideoFirstQuartile, adVideoMidpoint, adVideoThirdQuartile, adUserAcceptInvitation, adUserMinimize, adUserClose, adClickThru, getAdIcons, getAdDuration, getAdRemainingTime, adDurationChange, adRemainingTimeChange } from '../helpers/vpaid/api' import { linearEvents, skip, start, mute, unmute, impression, midpoint, complete, firstQuartile, thirdQuartile, pause, resume, clickThrough, iconClick, closeLinear, iconView, creativeView } from '../../tracker/linearEvents' import {acceptInvitation, adCollapse} from '../../tracker/nonLinearEvents' import {ErrorCode} from '../../tracker' import type {VastChain} from '../../types' import {addIcons} from '../helpers/icons/addIcons' import {retrieveIcons} from '../helpers/icons/retrieveIcons' import {volumeChanged, adProgress} from '../adUnitEvents' import {MockVpaidCreativeAd} from './MockVpaidCreativeAd' jest.mock('../helpers/vpaid/loadCreative') jest.mock('../helpers/vpaid/handshake') jest.mock('../helpers/vpaid/initAd') jest.mock('../helpers/vpaid/callAndWait') const mockDrawIcons = jest.fn() const mockRemoveIcons = jest.fn() const mockHasPendingRedraws = jest.fn(() => false) jest.mock('../helpers/icons/addIcons', () => ({ addIcons: jest.fn(() => ({ drawIcons: mockDrawIcons, hasPendingIconRedraws: mockHasPendingRedraws, removeIcons: mockRemoveIcons })) })) jest.mock('../helpers/icons/retrieveIcons', () => ({retrieveIcons: jest.fn()})) describe('VpaidAdUnit', () => { let vpaidChain: VastChain let videoAdContainer: VideoAdContainer beforeEach(() => { ;(callAndWait as jest.Mock).mockImplementation( jest.requireActual('../helpers/vpaid/callAndWait').callAndWait ) vpaidChain = [ { ad: vpaidInlineAd, parsedXML: vpaidInlineParsedXML, requestTag: 'https://test.example.com/vastadtaguri', XML: vastVpaidInlineXML } ] videoAdContainer = new VideoAdContainer(document.createElement('DIV')) videoAdContainer.slotElement = document.createElement('DIV') }) afterEach(() => { jest.restoreAllMocks() jest.clearAllMocks() }) test('must load the creative and publish the passed vastChain and container', () => { const adUnit = new VpaidAdUnit(vpaidChain, videoAdContainer) expect(adUnit.vastChain).toBe(vpaidChain) expect(adUnit.videoAdContainer).toBe(videoAdContainer) expect(loadCreative).toHaveBeenCalledTimes(1) expect(loadCreative).toHaveBeenCalledWith(vpaidChain, videoAdContainer) }) describe('start', () => { let mockCreativeAd: MockVpaidCreativeAd let adUnit: VpaidAdUnit beforeEach(() => { mockCreativeAd = new MockVpaidCreativeAd() ;(initAd as jest.Mock).mockImplementation(() => { mockCreativeAd.emit(adLoaded) }) ;(mockCreativeAd.startAd as jest.Mock).mockImplementation(() => { mockCreativeAd.emit(adStarted) }) ;(mockCreativeAd.stopAd as jest.Mock).mockImplementationOnce(() => { mockCreativeAd.emit(adStopped) }) ;(loadCreative as jest.Mock).mockReturnValue( Promise.resolve(mockCreativeAd) ) ;(retrieveIcons as jest.Mock).mockReturnValue(undefined) adUnit = new VpaidAdUnit(vpaidChain, videoAdContainer) }) test('must mute the creative if the videoElement is muted', async () => { const {videoElement} = videoAdContainer videoElement.muted = true await adUnit.start() expect(mockCreativeAd.setAdVolume).toHaveBeenCalledTimes(1) expect(mockCreativeAd.setAdVolume).toHaveBeenCalledWith(0) }) test('must set the volume of the creative to match the volume of the videoElement', async () => { const {videoElement} = videoAdContainer videoElement.muted = false videoElement.volume = 0.6 await adUnit.start() expect(mockCreativeAd.setAdVolume).toHaveBeenCalledTimes(1) expect(mockCreativeAd.setAdVolume).toHaveBeenCalledWith(0.6) }) test('must start the ad', async () => { expect(adUnit.isStarted()).toBe(false) await adUnit.start() expect(adUnit.isStarted()).toBe(true) expect(adUnit.creativeAd).toBe(mockCreativeAd) expect(handshake).toHaveBeenCalledTimes(1) expect(handshake).toHaveBeenCalledWith(mockCreativeAd, '2.0') expect(initAd).toHaveBeenCalledTimes(1) expect(initAd).toHaveBeenCalledWith( mockCreativeAd, videoAdContainer, vpaidChain ) expect(mockCreativeAd.startAd).toHaveBeenCalledTimes(1) expect(mockCreativeAd.stopAd).toHaveBeenCalledTimes(0) }) test('must not call startAd if the videoContainer was destroyed while loading the ad', async () => { expect(adUnit.isStarted()).toBe(false) videoAdContainer.destroy() await adUnit.start() expect(adUnit.isStarted()).toBe(false) expect(adUnit.creativeAd).toBe(mockCreativeAd) expect(handshake).toHaveBeenCalledTimes(1) expect(handshake).toHaveBeenCalledWith(mockCreativeAd, '2.0') expect(initAd).toHaveBeenCalledTimes(1) expect(initAd).toHaveBeenCalledWith( mockCreativeAd, videoAdContainer, vpaidChain ) expect(mockCreativeAd.startAd).toHaveBeenCalledTimes(0) expect(mockCreativeAd.stopAd).toHaveBeenCalledTimes(0) }) test('must call stopAd if adStarted event does not get acknowledged', async () => { mockCreativeAd.startAd.mockImplementation(() => { throw new Error('Error starting ad') }) adUnit = new VpaidAdUnit(vpaidChain, videoAdContainer) expect(adUnit.isStarted()).toBe(false) await adUnit.start() expect(adUnit.isStarted()).toBe(false) expect(adUnit.isFinished()).toBe(true) expect(adUnit.creativeAd).toBe(mockCreativeAd) expect(handshake).toHaveBeenCalledTimes(1) expect(handshake).toHaveBeenCalledWith(mockCreativeAd, '2.0') expect(initAd).toHaveBeenCalledTimes(1) expect(initAd).toHaveBeenCalledWith( mockCreativeAd, videoAdContainer, vpaidChain ) expect(mockCreativeAd.startAd).toHaveBeenCalledTimes(1) expect(mockCreativeAd.stopAd).toHaveBeenCalledTimes(1) }) test('must throw if the adUnit is started', async () => { await adUnit.start() try { await adUnit.start() } catch (error: any) { expect(error.message).toBe('VpaidAdUnit already started') } }) describe("with creativeAd's adIcon property", () => { test('undefined (VPAID 1) must not render the icons', async () => { delete (mockCreativeAd as any)[getAdIcons] await adUnit.start() expect(adUnit.icons).toBeUndefined() }) test('false, must not render the icons', async () => { mockCreativeAd[getAdIcons].mockReturnValue(false) await adUnit.start() expect(adUnit.icons).toBeUndefined() }) describe('true,', () => { test('without vast icons, must not add the icons', async () => { mockCreativeAd[getAdIcons].mockReturnValue(true) await adUnit.start() expect(retrieveIcons).toHaveBeenCalledTimes(1) expect(retrieveIcons).toHaveBeenCalledWith(adUnit.vastChain) expect(addIcons).not.toHaveBeenCalled() }) test('with vast icons, must render the icons', async () => { ;(addIcons as jest.Mock).mockClear() ;(retrieveIcons as jest.Mock).mockClear() const icons = [ { height: 20, width: 20, xPosition: 'left', yPosition: 'top' } ] ;(retrieveIcons as jest.Mock).mockReturnValue(icons) mockCreativeAd[getAdIcons].mockReturnValue(true) adUnit = new VpaidAdUnit(vpaidChain, videoAdContainer) await adUnit.start() expect(retrieveIcons).toHaveBeenCalledTimes(1) expect(retrieveIcons).toHaveBeenCalledWith(adUnit.vastChain) expect(addIcons).toHaveBeenCalledTimes(1) expect(addIcons).toHaveBeenCalledWith(icons, { logger: adUnit.logger, onIconClick: expect.any(Function), onIconView: expect.any(Function), videoAdContainer }) expect(mockDrawIcons).toHaveBeenCalledTimes(1) }) }) }) }) describe('with icons', () => { let mockCreativeAd: MockVpaidCreativeAd let adUnit: VpaidAdUnit beforeEach(() => { jest.useFakeTimers() mockCreativeAd = new MockVpaidCreativeAd() ;(initAd as jest.Mock).mockImplementation(() => { mockCreativeAd.emit(adLoaded) }) mockCreativeAd.startAd.mockImplementationOnce(() => { mockCreativeAd.emit(adStarted) }) mockCreativeAd.stopAd.mockImplementationOnce(() => { mockCreativeAd.emit(adStopped) }) mockCreativeAd.resizeAd.mockImplementationOnce(() => { mockCreativeAd.emit(adSizeChange) }) ;(loadCreative as jest.Mock).mockReturnValue( Promise.resolve(mockCreativeAd) ) adUnit = new VpaidAdUnit(vpaidChain, videoAdContainer) }) afterEach(() => { jest.clearAllTimers() }) test('must remove the icons on ad finish', async () => { const icons = [ { height: 20, width: 20, xPosition: 'left', yPosition: 'top' } ] ;(retrieveIcons as jest.Mock).mockReturnValue(icons) mockCreativeAd[getAdIcons].mockReturnValue(true) adUnit = new VpaidAdUnit(vpaidChain, videoAdContainer) await adUnit.start() await adUnit.cancel() expect(mockRemoveIcons).toHaveBeenCalledTimes(1) }) test('must redraw the icons on adUnit resize', async () => { const icons = [ { height: 20, width: 20, xPosition: 'left', yPosition: 'top' } ] ;(retrieveIcons as jest.Mock).mockReturnValue(icons) mockCreativeAd[getAdIcons].mockReturnValue(true) adUnit = new VpaidAdUnit(vpaidChain, videoAdContainer) await adUnit.start() expect(mockDrawIcons).toHaveBeenCalledTimes(1) await adUnit.resize(640, 480, 'normal') expect(mockDrawIcons).toHaveBeenCalledTimes(2) }) test(`must emit '${iconClick}' event on click`, async () => { ;(addIcons as jest.Mock).mockClear() const icons = [ { height: 20, width: 20, xPosition: 'left', yPosition: 'top' } ] ;(retrieveIcons as jest.Mock).mockReturnValue(icons) mockCreativeAd[getAdIcons].mockReturnValue(true) adUnit = new VpaidAdUnit(vpaidChain, videoAdContainer) await adUnit.start() expect(adUnit.icons).toBe(icons) const passedConfig = (addIcons as jest.Mock).mock.calls[0][1] const promise = new Promise((resolve) => { adUnit.on(iconClick, (...args) => { resolve(args) }) }) passedConfig.onIconClick(icons[0]) const passedArguments = await promise expect(passedArguments).toEqual([ { adUnit, data: icons[0], type: iconClick } ]) }) test(`must emit '${iconView}' event on view`, async () => { ;(addIcons as jest.Mock).mockClear() const icons = [ { height: 20, width: 20, xPosition: 'left', yPosition: 'top' } ] ;(retrieveIcons as jest.Mock).mockReturnValue(icons) mockCreativeAd[getAdIcons].mockReturnValue(true) adUnit = new VpaidAdUnit(vpaidChain, videoAdContainer) await adUnit.start() expect(adUnit.icons).toBe(icons) const passedConfig = (addIcons as jest.Mock).mock.calls[0][1] const promise = new Promise((resolve) => { adUnit.on(iconView, (...args) => { resolve(args) }) }) passedConfig.onIconView(icons[0]) const passedArguments = await promise expect(passedArguments).toEqual([ { adUnit, data: icons[0], type: iconView } ]) }) test('must periodically redraw the icons while it has pendingIconRedraws', async () => { const icons = [ { height: 20, width: 20, xPosition: 'left', yPosition: 'top' } ] ;(retrieveIcons as jest.Mock).mockReturnValue(icons) mockCreativeAd[getAdIcons].mockReturnValue(true) mockHasPendingRedraws.mockReturnValueOnce(true) mockHasPendingRedraws.mockReturnValueOnce(false) adUnit = new VpaidAdUnit(vpaidChain, videoAdContainer) await adUnit.start() expect(mockDrawIcons).toHaveBeenCalledTimes(1) jest.runOnlyPendingTimers() expect(mockDrawIcons).toHaveBeenCalledTimes(2) jest.runOnlyPendingTimers() expect(mockDrawIcons).toHaveBeenCalledTimes(2) }) test('must avoid redraw the icons if the adUnit is finished', async () => { const icons = [ { height: 20, width: 20, xPosition: 'left', yPosition: 'top' } ] ;(retrieveIcons as jest.Mock).mockReturnValue(icons) mockCreativeAd[getAdIcons].mockReturnValue(true) mockHasPendingRedraws.mockReturnValueOnce(true) mockHasPendingRedraws.mockReturnValueOnce(false) adUnit = new VpaidAdUnit(vpaidChain, videoAdContainer) await adUnit.start() expect(mockDrawIcons).toHaveBeenCalledTimes(1) await adUnit.cancel() expect(mockDrawIcons).toHaveBeenCalledTimes(1) jest.runAllTimers() expect(mockDrawIcons).toHaveBeenCalledTimes(1) }) }) describe('method', () => { let mockCreativeAd: MockVpaidCreativeAd let adUnit: VpaidAdUnit beforeEach(() => { mockCreativeAd = new MockVpaidCreativeAd() ;(initAd as jest.Mock).mockImplementation(() => { mockCreativeAd.emit(adLoaded) }) mockCreativeAd.startAd.mockImplementationOnce(() => { mockCreativeAd.emit(adStarted) }) mockCreativeAd.stopAd.mockImplementationOnce(() => { mockCreativeAd.emit(adStopped) }) mockCreativeAd.resumeAd.mockImplementationOnce(() => { mockCreativeAd.emit(adPlaying) }) mockCreativeAd.pauseAd.mockImplementationOnce(() => { mockCreativeAd.emit(adPaused) }) mockCreativeAd.skipAd.mockImplementationOnce(() => { mockCreativeAd.emit(adSkipped) }) mockCreativeAd.resizeAd.mockImplementationOnce(() => { mockCreativeAd.emit(adSizeChange) }) ;(loadCreative as jest.Mock).mockReturnValue( Promise.resolve(mockCreativeAd) ) adUnit = new VpaidAdUnit(vpaidChain, videoAdContainer) }) describe('resume', () => { test('must call resumeAd', async () => { await adUnit.start() adUnit.resume() expect(adUnit.creativeAd?.[resumeAd]).toHaveBeenCalledTimes(1) }) }) describe('pause', () => { test('must call pauseAd', async () => { await adUnit.start() adUnit.pause() expect(adUnit.creativeAd?.[pauseAd]).toHaveBeenCalledTimes(1) }) }) describe('skip', () => { test('must call skipAd', async () => { await adUnit.start() adUnit.skip() expect(adUnit.creativeAd?.[skipAd]).toHaveBeenCalledTimes(1) }) }) describe('getVolume', () => { test('must call getAdVolume', async () => { await adUnit.start() mockCreativeAd.getAdVolume.mockClear() adUnit.getVolume() expect(mockCreativeAd.getAdVolume).toHaveBeenCalledTimes(1) }) }) describe('setVolume', () => { test('must call setAdVolume', async () => { await adUnit.start() mockCreativeAd.setAdVolume.mockClear() adUnit.setVolume(0.5) expect(mockCreativeAd.setAdVolume).toHaveBeenCalledTimes(1) expect(mockCreativeAd.setAdVolume).toHaveBeenCalledWith(0.5) }) }) describe('resize', () => { test('must call resizeAd', async () => { await adUnit.start() await adUnit.resize(100, 150, 'normal') expect(callAndWait).toHaveBeenCalledWith( mockCreativeAd, resizeAd, adSizeChange, 100, 150, 'normal' ) }) }) describe('cancel', () => { jest.useFakeTimers() test('must throw if the adUnit is finished', async () => { expect.assertions(1) await adUnit.start() await adUnit.cancel() try { await adUnit.cancel() } catch (error: any) { expect(error.message).toBe('VideoAdUnit is finished') } }) test('must call stopAd and finish the adUnit', async () => { await adUnit.start() await adUnit.cancel() expect(mockCreativeAd.stopAd).toHaveBeenCalledTimes(1) expect(adUnit.isFinished()).toBe(true) }) it('must finish the adUnit if the creative does not emit adStopped event after some time', async () => { mockCreativeAd.stopAd = jest.fn() mockCreativeAd.stopAd.mockImplementationOnce(() => { // empty on purpose }) await adUnit.start() const cancelPromise = adUnit.cancel() expect(mockCreativeAd.stopAd).toHaveBeenCalledTimes(1) expect(adUnit.isFinished()).toBe(false) jest.runOnlyPendingTimers() await cancelPromise expect(adUnit.isFinished()).toBe(true) }) }) describe('onFinish', () => { test("must throw if you don't pass a callback function ", async () => { await adUnit.start() expect(() => adUnit.onFinish(undefined as any)).toThrow( 'Expected a callback function' ) }) test('must be called if the ad unit gets canceled', async () => { const callback = jest.fn() adUnit.onFinish(callback) await adUnit.start() expect(callback).not.toHaveBeenCalled() await adUnit.cancel() expect(callback).toHaveBeenCalledTimes(1) }) test('must be called once the ad unit completes', async () => { const callback = jest.fn() adUnit.onFinish(callback) await adUnit.start() expect(callback).not.toHaveBeenCalled() ;(adUnit.creativeAd as any).emit(adVideoComplete) ;(adUnit.creativeAd as any).emit(adStopped) expect(callback).toHaveBeenCalledTimes(1) }) test('must be called once the ad unit stops', async () => { const callback = jest.fn() adUnit.onFinish(callback) await adUnit.start() expect(callback).not.toHaveBeenCalled() ;(adUnit.creativeAd as any).emit(adStopped) expect(callback).toHaveBeenCalledTimes(1) }) test('must be called if the user closes the ad unit', async () => { const callback = jest.fn() adUnit.onFinish(callback) await adUnit.start() expect(callback).not.toHaveBeenCalled() ;(adUnit.creativeAd as any).emit(adUserClose) expect(callback).toHaveBeenCalledTimes(1) }) }) describe('onError', () => { test("must throw if you don't pass a callback function ", async () => { await adUnit.start() expect(() => adUnit.onError(undefined as any)).toThrow( 'Expected a callback function' ) }) test('must call the callback if there is a problem starting the ad', async () => { expect.assertions(3) const handshakeVersionError = new Error( 'Handshake version not supported' ) const callback = jest.fn() adUnit.onError(callback) ;(handshake as jest.Mock).mockImplementationOnce(() => { throw handshakeVersionError }) try { await adUnit.start() } catch (error) { expect(error).toBe(handshakeVersionError) expect(callback).toHaveBeenCalledTimes(1) expect(callback).toHaveBeenCalledWith(handshakeVersionError, { adUnit, vastChain: adUnit.vastChain }) } }) test('must call the callback if there is an error with the creativeAd', async () => { const callback = jest.fn() adUnit.onError(callback) await adUnit.start() expect(callback).not.toHaveBeenCalled() ;(adUnit.creativeAd as any).emit(adError) expect(callback).toHaveBeenCalledTimes(1) expect(callback).toHaveBeenCalledWith( expect.objectContaining({ message: 'VPAID general error' }), { adUnit, vastChain: adUnit.vastChain } ) }) }) }) describe('creative vpaid event', () => { let mockCreativeAd: MockVpaidCreativeAd let adUnit: VpaidAdUnit beforeEach(() => { mockCreativeAd = new MockVpaidCreativeAd() ;(initAd as jest.Mock).mockImplementation(() => { mockCreativeAd.emit(adLoaded) }) mockCreativeAd.startAd.mockImplementationOnce(() => { mockCreativeAd.emit(adStarted) }) mockCreativeAd.stopAd.mockImplementationOnce(() => { mockCreativeAd.emit(adStopped) }) ;(loadCreative as jest.Mock).mockReturnValue( Promise.resolve(mockCreativeAd) ) adUnit = new VpaidAdUnit(vpaidChain, videoAdContainer) }) for (const vpaidEvent of EVENTS.filter((event) => event !== adLoaded)) { test(`${vpaidEvent} must be emitted by the ad unit`, async () => { const callback = jest.fn() adUnit.on(vpaidEvent, callback) await adUnit.start() ;(adUnit.creativeAd as any).emit(vpaidEvent) expect(callback).toHaveBeenCalledWith({ adUnit, type: vpaidEvent }) }) } ;[ { vastEvt: skip, vpaidEvt: adSkipped }, { vastEvt: adProgress, vpaidEvt: adDurationChange }, { vastEvt: adProgress, vpaidEvt: adRemainingTimeChange }, { vastEvt: creativeView, vpaidEvt: adStarted }, { vastEvt: impression, vpaidEvt: adImpression }, { vastEvt: skip, vpaidEvt: adSkipped }, { vastEvt: start, vpaidEvt: adVideoStart }, { vastEvt: firstQuartile, vpaidEvt: adVideoFirstQuartile }, { vastEvt: midpoint, vpaidEvt: adVideoMidpoint }, { vastEvt: thirdQuartile, vpaidEvt: adVideoThirdQuartile }, { vastEvt: complete, vpaidEvt: adVideoComplete }, { vastEvt: acceptInvitation, vpaidEvt: adUserAcceptInvitation }, { vastEvt: adCollapse, vpaidEvt: adUserMinimize }, { vastEvt: closeLinear, vpaidEvt: adUserClose }, { vastEvt: pause, vpaidEvt: adPaused }, { vastEvt: resume, vpaidEvt: adPlaying }, { payload: { data: { playerHandles: true, url: 'https://test.example.com/clickThrough' } }, vastEvt: clickThrough, vpaidEvt: adClickThru } ].forEach(({vpaidEvt: vpaidEvent, vastEvt: vastEvent, payload}) => { describe(vpaidEvent, () => { test(`must emit ${vastEvent} event`, async () => { const callback = jest.fn() adUnit.on(vastEvent, callback) await adUnit.start() ;(adUnit.creativeAd as any).emit(vpaidEvent, payload) expect(callback).toHaveBeenCalledWith({ adUnit, type: vastEvent }) }) }) }) test('must emit start event once', async () => { const callback = jest.fn() adUnit.on(start, callback) await adUnit.start() ;(adUnit.creativeAd as any).emit(adVideoStart) ;(adUnit.creativeAd as any).emit(adVideoStart) ;(adUnit.creativeAd as any).emit(adVideoStart) expect(callback).toHaveBeenCalledTimes(1) }) test('must fake `adVideoStarted` on adImpression if not called already', async () => { const callback = jest.fn() adUnit.on(start, callback) await adUnit.start() ;(adUnit.creativeAd as any).emit(adImpression) expect(callback).toHaveBeenCalledTimes(1) expect(callback).toHaveBeenCalledWith({ adUnit, type: start }) }) test('must not fake `adVideoStarted` on adImpression if called already', async () => { const callback = jest.fn() adUnit.on(start, callback) await adUnit.start() ;(adUnit.creativeAd as any).emit(adVideoStart) expect(callback).toHaveBeenCalledTimes(1) ;(adUnit.creativeAd as any).emit(adImpression) expect(callback).toHaveBeenCalledTimes(1) }) describe('paused', () => { it('must return true if the creative is paused and false otherwise', async () => { await adUnit.start() expect(adUnit.paused()).toBe(true) ;(adUnit.creativeAd as any).emit(adVideoStart) expect(adUnit.paused()).toBe(false) ;(adUnit.creativeAd as any).emit(adPaused) expect(adUnit.paused()).toBe(true) ;(adUnit.creativeAd as any).emit(adPlaying) expect(adUnit.paused()).toBe(false) await adUnit.cancel() expect(adUnit.paused()).toBe(true) }) }) describe(adClickThru, () => { let origOpen: any beforeEach(() => { origOpen = window.open window.open = jest.fn() }) afterEach(() => { window.open = origOpen }) test('must not open a new tab if `playerHandles` is false', async () => { await adUnit.start() ;(adUnit.creativeAd as any).emit( adClickThru, 'https://test.example.com/clickUrl', undefined, false ) expect(window.open).not.toHaveBeenCalled() }) describe('with `playerHandles` true', () => { beforeEach(async () => { await adUnit.start() }) test('if paused, must resume the adUnit', () => { ;(adUnit.creativeAd as any).emit(adVideoStart) ;(adUnit.creativeAd as any).emit(adPaused) expect(adUnit.paused()).toBe(true) ;(adUnit.creativeAd as any).emit( adClickThru, 'https://test.example.com/clickUrl', undefined, true ) expect(window.open).not.toHaveBeenCalled() expect(adUnit.creativeAd?.pauseAd).toHaveBeenCalledTimes(0) expect(adUnit.creativeAd?.resumeAd).toHaveBeenCalledTimes(1) }) describe('if playing', () => { test('must pause the adUnit', () => { ;(adUnit.creativeAd as any).emit(adVideoStart) ;(adUnit.creativeAd as any).emit( adClickThru, 'https://test.example.com/clickUrl', undefined, true ) expect(window.open).toHaveBeenCalled() expect(adUnit.creativeAd?.pauseAd).toHaveBeenCalledTimes(1) expect(adUnit.creativeAd?.resumeAd).toHaveBeenCalledTimes(0) }) test('must open the provided url in a new tab', () => { ;(adUnit.creativeAd as any).emit(adVideoStart) ;(adUnit.creativeAd as any).emit( adClickThru, 'https://test.example.com/clickUrl', undefined, true ) expect(window.open).toHaveBeenCalledTimes(1) expect(window.open).toHaveBeenCalledWith( 'https://test.example.com/clickUrl', '_blank' ) }) test('must use vast clickthrough url if no url is provided', () => { ;(adUnit.creativeAd as any).emit(adVideoStart) ;(adUnit.creativeAd as any).emit(adClickThru, '', '', true) expect(window.open).toHaveBeenCalledTimes(1) expect(window.open).toHaveBeenCalledWith( 'https://test.example.com/clickthrough', '_blank' ) }) }) test('if paused, must not resume the adUnit and open the provided url in a new tab', () => { adUnit.pauseOnAdClick = false ;(adUnit.creativeAd as any).emit(adVideoStart) ;(adUnit.creativeAd as any).emit(adPaused) expect(adUnit.paused()).toBe(true) ;(adUnit.creativeAd as any).emit( adClickThru, 'https://test.example.com/clickUrl', undefined, true ) expect(window.open).toHaveBeenCalledTimes(1) expect(window.open).toHaveBeenCalledWith( 'https://test.example.com/clickUrl', '_blank' ) expect(adUnit.creativeAd?.pauseAd).not.toHaveBeenCalled() expect(adUnit.creativeAd?.resumeAd).not.toHaveBeenCalled() }) test('if playing, must not pause the adUnit and open the provided url in a new tab', () => { adUnit.pauseOnAdClick = false ;(adUnit.creativeAd as any).emit(adVideoStart) expect(adUnit.paused()).toBe(false) ;(adUnit.creativeAd as any).emit( adClickThru, 'https://test.example.com/clickUrl', undefined, true ) expect(window.open).toHaveBeenCalledTimes(1) expect(window.open).toHaveBeenCalledWith( 'https://test.example.com/clickUrl', '_blank' ) expect(adUnit.creativeAd?.pauseAd).not.toHaveBeenCalled() expect(adUnit.creativeAd?.resumeAd).not.toHaveBeenCalled() }) }) }) describe(adError, () => { const vastEvent = linearEvents.error test(`must emit ${vastEvent} event`, async () => { const callback = jest.fn() adUnit.on(vastEvent, callback) await adUnit.start() ;(adUnit.creativeAd as any).emit(adError) expect(callback).toHaveBeenCalledWith({ adUnit, type: vastEvent }) const {error} = adUnit expect(error?.message).toBe('VPAID general error') expect(error?.code).toBe(ErrorCode.VPAID_ERROR) expect(adUnit.errorCode).toBe(ErrorCode.VPAID_ERROR) }) it('must use the emitted error if provided', async () => { const callback = jest.fn() adUnit.on(vastEvent, callback) await adUnit.start() const creativeError = new AdUnitError('test error') creativeError.code = ErrorCode.VAST_TOO_MANY_REDIRECTS ;(adUnit.creativeAd as any).emit(adError, creativeError) expect(callback).toHaveBeenCalledWith({ adUnit, type: vastEvent }) const {error} = adUnit expect(error).toBe(creativeError) expect(error?.code).toBe(ErrorCode.VAST_TOO_MANY_REDIRECTS) expect(adUnit.errorCode).toBe(ErrorCode.VAST_TOO_MANY_REDIRECTS) }) }) describe(adVolumeChange, () => { test(`must emit ${volumeChanged} event`, async () => { const callback = jest.fn() await adUnit.start() adUnit.on(volumeChanged, callback) adUnit.creativeAd?.setAdVolume(0) expect(callback).toHaveBeenCalledTimes(1) expect(callback).toHaveBeenCalledWith({ adUnit, type: volumeChanged }) }) test(`must emit ${mute} event if it becomes muted`, async () => { const callback = jest.fn() adUnit.on(mute, callback) await adUnit.start() adUnit.creativeAd?.setAdVolume(0) expect(callback).toHaveBeenCalledTimes(1) expect(callback).toHaveBeenCalledWith({ adUnit, type: mute }) }) test(`must emit ${unmute} event if it becomes unmuted`, async () => { const callback = jest.fn() adUnit.on(unmute, callback) await adUnit.start() adUnit.creativeAd?.setAdVolume(0) expect(callback).not.toHaveBeenCalled() adUnit.creativeAd?.setAdVolume(0.5) expect(callback).toHaveBeenCalledTimes(1) expect(callback).toHaveBeenCalledWith({ adUnit, type: unmute }) }) test('must not emit any event on normal volume change', async () => { const callback = jest.fn() adUnit.on(unmute, callback) await adUnit.start() adUnit.creativeAd?.setAdVolume(0.5) adUnit.creativeAd?.setAdVolume(0.5) expect(callback).not.toHaveBeenCalled() }) }) describe('duration', () => { it('must return 0 if there is no creative', () => { expect(adUnit.duration()).toBe(0) }) it('must return the creative duration', async () => { await adUnit.start() ;(adUnit.creativeAd?.[getAdDuration] as jest.Mock).mockReturnValue(30) expect(adUnit.duration()).toBe(30) }) it('must return 0 if the creative returns a negative duration', async () => { await adUnit.start() ;(adUnit.creativeAd?.[getAdDuration] as jest.Mock).mockReturnValue(-1) expect(adUnit.duration()).toBe(0) ;(adUnit.creativeAd?.[getAdDuration] as jest.Mock).mockReturnValue(-2) expect(adUnit.duration()).toBe(0) }) }) describe('currentTime', () => { it('must return 0 if there is no creative', () => { expect(adUnit.currentTime()).toBe(0) }) it('must return the creative current time', async () => { await adUnit.start() ;(adUnit.creativeAd?.[getAdDuration] as jest.Mock).mockReturnValue(30) ;(adUnit.creativeAd?.[getAdRemainingTime] as jest.Mock).mockReturnValue( 25 ) expect(adUnit.currentTime()).toBe(5) ;(adUnit.creativeAd?.[getAdRemainingTime] as jest.Mock).mockReturnValue( 5 ) expect(adUnit.currentTime()).toBe(25) }) it('must return 0 if the creative returns a negative adRemainingTime', async () => { await adUnit.start() ;(adUnit.creativeAd?.[getAdDuration] as jest.Mock).mockReturnValue(-1) ;(adUnit.creativeAd?.[getAdRemainingTime] as jest.Mock).mockReturnValue( -1 ) expect(adUnit.currentTime()).toBe(0) ;(adUnit.creativeAd?.[getAdDuration] as jest.Mock).mockReturnValue(-2) ;(adUnit.creativeAd?.[getAdRemainingTime] as jest.Mock).mockReturnValue( -2 ) expect(adUnit.currentTime()).toBe(0) }) }) }) })