UNPKG

@dailymotion/vast-client

Version:
953 lines (853 loc) 32.2 kB
import { VASTParser } from '../src/parser/vast_parser'; import { urlFor, getNodesFromXml } from './utils/utils'; import { util } from '../src/util/util'; import { parserUtils } from '../src/parser/parser_utils'; import * as Bitrate from '../src/parser/bitrate'; import { Fetcher } from '../src/fetcher/fetcher'; import { linearAd } from './samples/linear_ads'; import { VASTClient } from '../src/vast_client'; import { readFile } from 'fs/promises'; const xml = getNodesFromXml(`<VAST>${linearAd}</VAST>`, 'text/xml'); const nodeUrlHandler = { get: async (file) => { try { const response = await readFile(file, 'utf-8'); return { xml: new DOMParser().parseFromString(response, 'text/xml'), details: { byteLength: 1234, statusCode: 200 }, }; } catch (err) { return { error: err, statusCode: 400 }; } }, }; const wrapperAVastUrl = urlFor('wrapper-a.xml'); const wrapperBVastUrl = urlFor('wrapper-b.xml'); const inlineSampleVastUrl = urlFor('sample.xml'); const inlineInvalidVastUrl = urlFor('invalid-xmlfile.xml'); describe('VASTParser', () => { let vastClient; let VastParser; let fetcher; let inlineXml, invalidXml, errorXml, wrapperXml, outdatedXml; beforeAll(async () => { inlineXml = await nodeUrlHandler.get('./spec/samples/sample.xml'); errorXml = await nodeUrlHandler.get('./spec/samples/empty-no-ad.xml'); outdatedXml = await nodeUrlHandler.get('./spec/samples/outdated-vast.xml'); wrapperXml = await nodeUrlHandler.get( './spec/samples/wrapper-attributes-multiple-ads.xml' ); invalidXml = await nodeUrlHandler.get('./spec/samples/invalid-xmlfile.xml'); }); beforeEach(() => { vastClient = new VASTClient(); fetcher = new Fetcher(); VastParser = new VASTParser({ fetcher }); jest.spyOn(VastParser, 'emit'); }); describe('initParsingStatus', () => { beforeEach(() => { jest.spyOn(VastParser, 'resetParsingStatus'); }); it('assigns options to properties', () => { jest.spyOn(Bitrate, 'updateEstimatedBitrate'); VastParser.initParsingStatus({ wrapperLimit: 5, allowMultipleAds: true, byteLength: 1000, requestDuration: 200, }); expect(VastParser.rootURL).toBe(''); expect(VastParser.remainingAds).toEqual([]); expect(VastParser.errorURLTemplates).toEqual([]); expect(VastParser.rootErrorURLTemplates).toEqual([]); expect(VastParser.maxWrapperDepth).toBe(5); expect(VastParser.vastVersion).toBeNull(); expect(VastParser.parsingOptions).toEqual({ allowMultipleAds: true }); expect(Bitrate.updateEstimatedBitrate).toBeCalledWith(1000, 200); expect(VastParser.resetParsingStatus).toHaveBeenCalled(); }); it('uses default values if no options are passed', () => { VastParser.initParsingStatus(); expect(VastParser.rootURL).toBe(''); expect(VastParser.remainingAds).toEqual([]); expect(VastParser.errorURLTemplates).toEqual([]); expect(VastParser.rootErrorURLTemplates).toEqual([]); expect(VastParser.maxWrapperDepth).toBe(10); expect(VastParser.vastVersion).toBeNull(); expect(VastParser.parsingOptions).toEqual({ allowMultipleAds: undefined, }); expect(VastParser.resetParsingStatus).toHaveBeenCalled(); }); }); describe('parseVastXml', () => { it('handles invalid XML vast', () => { try { VastParser.parseVastXml(invalidXml.xml, { isRootVAST: true, url: inlineInvalidVastUrl, wrapperDepth: 0, }); } catch (e) { expect(e.message).toBe('Invalid VAST XMLDocument'); expect(VastParser.emit).toHaveBeenLastCalledWith('VAST-ad-parsed', { type: 'ERROR', url: inlineInvalidVastUrl, wrapperDepth: 0, wrapperChainId: 0, }); } }); it('throw a error for non supported XML vast', () => { try { VastParser.parseVastXml(outdatedXml.xml, { isRootVAST: true, url: null, wrapperDepth: 0, }); } catch (e) { expect(e.message).toBe('VAST response version not supported'); expect(VastParser.emit).toHaveBeenLastCalledWith('VAST-ad-parsed', { type: 'ERROR', url: null, wrapperDepth: 0, wrapperChainId: 0, }); } }); it('gets vast version from original vast', () => { VastParser.parseVastXml(inlineXml.xml, { isRootVAST: true, url: inlineSampleVastUrl, wrapperDepth: 0, }); expect(VastParser.vastVersion).toBe('4.3'); }); it('handles Error tag for root VAST', () => { //initParsingStatus always will be called before parseVastXml VastParser.rootErrorURLTemplates = []; VastParser.parseVastXml(errorXml.xml, { isRootVAST: true }); expect(VastParser.rootErrorURLTemplates).toEqual([ 'http://example.com/empty-no-ad', ]); }); it('handles Error tag for not root VAST', () => { VastParser.initParsingStatus(); VastParser.parseVastXml(errorXml.xml, { isRootVAST: false }); expect(VastParser.errorURLTemplates).toEqual([ 'http://example.com/empty-no-ad', ]); }); it('handles Ad tag', () => { const ads = VastParser.parseVastXml(inlineXml.xml, { isRootVAST: true, url: inlineSampleVastUrl, wrapperDepth: 0, allowMultipleAds: true, }); expect(ads).toHaveLength(2); expect(VastParser.emit).toHaveBeenCalledTimes(2); expect(VastParser.emit).toHaveBeenCalledWith('VAST-ad-parsed', { adIndex: 0, type: 'INLINE', url: inlineSampleVastUrl, wrapperDepth: 0, vastVersion: '4.3', wrapperChainId: 0, }); expect(VastParser.emit).toHaveBeenCalledWith('VAST-ad-parsed', { adIndex: 1, type: 'INLINE', url: inlineSampleVastUrl, wrapperDepth: 0, vastVersion: '4.3', wrapperChainId: 0, }); }); }); describe('parse', () => { it('calls parseVastXml with passed options', () => { jest.spyOn(VastParser, 'parseVastXml'); return VastParser.parse(inlineXml.xml, { url: inlineSampleVastUrl, previousUrl: wrapperBVastUrl, resolveAll: true, wrapperSequence: 1, wrapperDepth: 0, isRootVAST: true, }).then(() => { expect(VastParser.parseVastXml).toHaveBeenCalledWith(inlineXml.xml, { isRootVAST: true, url: inlineSampleVastUrl, wrapperDepth: 0, allowMultipleAds: undefined, followAdditionalWrappers: undefined, wrapperChainId: 0, }); }); }); it('rejects if parsing xml failed', () => { jest.spyOn(VastParser, 'parseVastXml'); return VastParser.parse(invalidXml.xml, { url: inlineInvalidVastUrl, previousUrl: wrapperBVastUrl, resolveAll: true, wrapperSequence: 1, wrapperDepth: 0, isRootVAST: true, }).catch((e) => { expect(e.message).toBe('Invalid VAST XMLDocument'); }); }); it('resolves first ad and saves remaining ads if resolveAll is false', () => { return VastParser.parse(inlineXml.xml, { url: inlineSampleVastUrl, previousUrl: wrapperBVastUrl, resolveAll: false, wrapperSequence: 1, wrapperDepth: 0, isRootVAST: true, allowMultipleAds: true, }).then((ads) => { expect(VastParser.remainingAds).toHaveLength(1); expect(ads).toHaveLength(1); }); }); it('parse wrapper sub elements based on allowMultipleAds and followAdditionalWrappers values', () => { return VastParser.parse(wrapperXml.xml).then((ads) => { expect(ads.length).toEqual(4); }); }); it('it replaces the ad sequence with the value of the wrapper sequence if it contains only one ad', () => { jest.spyOn(VastParser, 'parseVastXml').mockReturnValue([{ sequence: 2 }]); return VastParser.parse(wrapperXml, { wrapperSequence: 4 }).then( (ads) => { expect(ads[0].sequence).toEqual(4); } ); }); it('does not keep wrapper sequence value when wrapper contain an adpod', () => { jest .spyOn(VastParser, 'parseVastXml') .mockReturnValue([{ sequence: 1 }, { sequence: 2 }, { sequence: 3 }]); return VastParser.parse(wrapperXml, { wrapperSequence: 4 }).then( (ads) => { expect(ads[0].sequence).toEqual(1); expect(ads[1].sequence).toEqual(2); expect(ads[2].sequence).toEqual(3); } ); }); }); describe('parseVAST', () => { let options; beforeEach(() => { options = { wrapperLimit: 5, allowMultipleAds: true, byteLength: 1234, requestDuration: 12000, }; jest.spyOn(VastParser, 'initParsingStatus'); jest .spyOn(VastParser, 'parse') .mockReturnValue(Promise.resolve([linearAd])); jest.spyOn(VastParser, 'buildVASTResponse').mockReturnValue({ ads: [linearAd], errorURLTemplates: [], version: null, }); jest.spyOn(Bitrate, 'updateEstimatedBitrate'); }); afterEach(() => { jest.clearAllMocks(); }); it('should return a VAST response object', (done) => { VastParser.parseVAST(xml, options).then((response) => { expect(response).toEqual({ ads: [linearAd], errorURLTemplates: [], version: null, }); expect(VastParser.initParsingStatus).toHaveBeenCalled(); expect(VastParser.parse).toHaveBeenCalled(); expect(VastParser.buildVASTResponse).toHaveBeenCalled(); done(); }); }); it('should have set the options if given', (done) => { VastParser.parseVAST(xml, options).then(() => { expect(VastParser.maxWrapperDepth).toBe(5); expect(VastParser.parsingOptions).toEqual({ allowMultipleAds: true }); expect(Bitrate.updateEstimatedBitrate).toHaveBeenCalledWith( 1234, 12000 ); done(); }); }); it('should have set the default options if not given', (done) => { VastParser.parseVAST(xml).then(() => { expect(VastParser.maxWrapperDepth).toBe(10); expect(VastParser.parsingOptions).toEqual({ allowMultipleAds: undefined, }); }); done(); }); }); describe('Tracking', () => { let trackCalls = null; let dataTriggered = null; const options = { urlhandler: nodeUrlHandler, }; beforeEach(() => { VastParser.removeAllListeners(); dataTriggered = []; trackCalls = []; VastParser.on('VAST-error', (variables) => dataTriggered.push(variables)); util.track = (templates, variables) => { trackCalls.push({ templates, variables, }); }; }); describe('No Ad', () => { it('should emits a VAST-error & track', (done) => { let vast = new DOMParser().parseFromString( `<VAST><Error>http://example.com/empty-no-ad</Error></VAST>`, 'text/xml' ); VastParser.parseVAST(vast) .then((response) => { // Response doesn't have any ads expect(response.ads).toEqual([]); expect(dataTriggered.length).toBe(1); // Error has been triggered expect(dataTriggered.length).toBe(1); expect(dataTriggered[0].ERRORCODE).toBe(303); expect(dataTriggered[0].extensions).toEqual([]); // Tracking has been done expect(trackCalls.length).toBe(1); expect(trackCalls[0].templates).toEqual([ 'http://example.com/empty-no-ad', ]); expect(trackCalls[0].variables).toEqual({ ERRORCODE: 303 }); done(); }) .catch((error) => { console.error(error); done(error); }); }); it('should emits VAST-error & track when wrapped', async () => { const url = './spec/samples/wrapper-empty.xml'; const response = await nodeUrlHandler.get( './spec/samples/wrapper-empty.xml' ); fetcher.setOptions({ ...options, url: url, previousUrl: url }); VastParser.fetchingCallback = fetcher.fetchVAST.bind(fetcher); const vastXML = await VastParser.parseVAST(response.xml, { url: url, previousUrl: url, }); // Response doesn't have any ads expect(vastXML.ads).toEqual([]); // error has been triggered expect(dataTriggered.length).toBe(1); expect(dataTriggered[0].ERRORCODE).toBe(303); expect(dataTriggered[0].extensions[0].children[0].name).toBe( 'paramWrapperEmptyNoAd' ); expect(dataTriggered[0].extensions[0].children[0].value).toBe( 'valueWrapperEmptyNoAd' ); // TRacking has been done expect(trackCalls.length).toBe(1); expect(trackCalls[0].templates).toEqual([ 'http://example.com/wrapper-empty_wrapper-error', 'http://example.com/empty-no-ad', ]); expect(trackCalls[0].variables).toEqual({ ERRORCODE: 303 }); }); }); describe('Ad with no creatives', () => { it('should emits a VAST-error & track', async () => { const url = './spec/samples/empty-no-creative.xml'; const response = await nodeUrlHandler.get(url); const vastXML = await VastParser.parseVAST(response.xml); // Response doesn't have any ads expect(vastXML.ads).toEqual([]); // Error has been triggered expect(dataTriggered.length).toBe(1); expect(dataTriggered[0].ERRORCODE).toBe(303); expect(dataTriggered[0].extensions[0].children[0].name).toBe( 'paramEmptyNoCreative' ); expect(dataTriggered[0].extensions[0].children[0].value).toBe( 'valueEmptyNoCreative' ); // Tracking has been done; expect(trackCalls.length).toBe(1); expect(trackCalls[0].templates).toEqual([ 'http://example.com/empty-no-creative_inline-error', ]); expect(trackCalls[0].variables).toEqual({ ERRORCODE: 303 }); }); it('should emits a VAST-ERROR & track when wrapped', async () => { const url = './spec/samples/wrapper-empty-no-creative.xml'; const response = await nodeUrlHandler.get(url); fetcher.setOptions({ ...options, url: url, previousUrl: url }); VastParser.fetchingCallback = fetcher.fetchVAST.bind(fetcher); const vastXML = await VastParser.parseVAST(response.xml, { url: url, previousUrl: url, }); // Response doesn't have any ads expect(vastXML.ads).toEqual([]); // Error has been triggered expect(dataTriggered.length).toBe(1); expect(dataTriggered[0].ERRORCODE).toBe(303); expect(dataTriggered[0].extensions[0].children[0].name).toBe( 'paramWrapperEmptyNoCreative' ); expect(dataTriggered[0].extensions[0].children[0].value).toBe( 'valueWrapperEmptyNoCreative' ); expect(dataTriggered[0].extensions[1].children[0].name).toBe( 'paramEmptyNoCreative' ); expect(dataTriggered[0].extensions[1].children[0].value).toBe( 'valueEmptyNoCreative' ); // Tracking has been done expect(trackCalls.length).toBe(1); expect(trackCalls[0].templates).toEqual([ 'http://example.com/wrapper-no-creative_wrapper-error', 'http://example.com/empty-no-creative_inline-error', ]); expect(trackCalls[0].variables).toEqual({ ERRORCODE: 303 }); }); }); describe('Wrapper URL unavailable/timeout', () => { it('should emits a VAST-error and track', async () => { const url = './spec/samples/wrapper-unavailable-url.xml'; const response = await nodeUrlHandler.get(url); fetcher.setOptions({ ...options, url: url, previousUrl: url }); VastParser.fetchingCallback = fetcher.fetchVAST.bind(fetcher); const vast = await VastParser.parseVAST(response.xml, { url: url, previousUrl: url, }); // Response doesn't have any ads expect(vast.ads).toEqual([]); // Error has been trigered expect(dataTriggered.length).toBe(1); expect(dataTriggered[0].ERRORCODE).toBe(301); expect(dataTriggered[0].extensions[0].children[0].name).toBe( 'paramWrapperInvalidXmlfile' ); expect(dataTriggered[0].extensions[0].children[0].value).toBe( 'valueWrapperInvalidXmlfile' ); // Tracking has been done expect(trackCalls.length).toBe(1); expect(trackCalls[0].templates).toEqual([ 'http://example.com/wrapper-invalid-xmlfile_wrapper-error', ]); expect(trackCalls[0].variables).toEqual({ ERRORCODE: 301 }); }); }); describe('Wrapper limit reached', () => { it('should emits a VAST-error & track', async () => { const url = './spec/samples/wrapper-b.xml'; const response = await nodeUrlHandler.get(url); fetcher.setOptions({ ...options, url: url, previousUrl: url }); VastParser.fetchingCallback = fetcher.fetchVAST.bind(fetcher); const vastXML = await VastParser.parseVAST(response.xml, { url: url, previousUrl: url, wrapperLimit: 1, }); // Response doesn't have any ads expect(vastXML.ads).toEqual([]); // Error has been triggered expect(dataTriggered.length).toBe(1); expect(dataTriggered[0].ERRORCODE).toBe(302); expect(dataTriggered[0].extensions.length).toBe(0); // Tracking has been done expect(trackCalls.length).toBe(1); expect(trackCalls[0].templates).toEqual([ 'http://example.com/wrapperB-error', ]); expect(trackCalls[0].variables).toEqual({ ERRORCODE: 302 }); }); }); describe('Legacy', () => { let response = null; beforeEach((done) => { VastParser.removeAllListeners(); vastClient .get('./spec/samples/wrapper-legacy.xml', options) .then((res) => { response = res; done(); }); }); describe('should correctly loads a wrapped ad, even with the VASTAdTagURL-Tag', () => { it('should have found 1 ad', () => { expect(response.ads.length).toBe(1); }); it('should have returned a VAST response object', () => { expect(response.ads.length).toBe(1); expect(response).toHaveProperty('ads'); expect(response).toHaveProperty('errorURLTemplates'); expect(response).toHaveProperty('version'); }); it('should have found 2 creatives', () => { expect(response.ads[0].creatives.length).toBe(2); }); it('should have parsed mediafile attribute', () => { const mediafile = response.ads[0].creatives[1].mediaFiles[0]; expect(mediafile.mimeType).toBe('video/mp4'); expect(mediafile.width).toBe(400); expect(mediafile.height).toBe(300); expect(mediafile.fileURL).toBe( 'https://iabtechlab.com/wp-content/uploads/2016/07/VAST-4.0-Short-Intro.mp4' ); expect(mediafile.bitrate).toBe(500); expect(mediafile.minBitrate).toBe(360); expect(mediafile.maxBitrate).toBe(1080); expect(mediafile.scalable).toBe(true); }); }); }); }); describe('resolveAds', () => { it('updates previousUrl value and calls resolveWrappers for each ad', () => { jest .spyOn(VastParser, 'resolveWrappers') .mockReturnValue(Promise.resolve(['ad1', 'ad2'])); return VastParser.resolveAds(['ad1', 'ad2'], { wrapperDepth: 1, previousUrl: wrapperBVastUrl, url: inlineSampleVastUrl, }).then(() => { expect(VastParser.resolveWrappers).toHaveBeenCalledTimes(2); expect(VastParser.resolveWrappers.mock.calls).toEqual([ ['ad1', 1, inlineSampleVastUrl, 0], ['ad2', 1, inlineSampleVastUrl, 0], ]); }); }); }); describe('resolveWrappers', () => { const ad = { id: null, sequence: 1, system: { value: 'VAST', version: null }, title: null, description: null, advertiser: null, pricing: null, survey: null, errorURLTemplates: ['http://example.com/wrapperA-error'], impressionURLTemplates: [], creatives: [], extensions: [], adVerifications: [], trackingEvents: { nonlinear: [], linear: [] }, videoClickTrackingURLTemplates: [], videoCustomClickURLTemplates: [], viewableImpression: [], }; beforeEach(() => { VastParser.previousUrl = wrapperAVastUrl; VastParser.initParsingStatus(); }); it('resolves with ad if there is no more wrappers', () => { return VastParser.resolveWrappers(ad, 0).then((res) => { expect(res).toEqual(ad); }); }); it('will add errorcode to resolved ad if parsing has reached maximum amount of unwrapping', () => { const adWithWrapper = { ...ad, nextWrapperURL: 'http://example.com/foo' }; VastParser.fetchingCallback = () => {}; VastParser.maxWrapperDepth = 10; return VastParser.resolveWrappers(adWithWrapper, 10, null).then((res) => { expect(res).toEqual({ ...ad, errorCode: 302, }); }); }); it('will successfully fetch the next wrapper url if it is provided', () => { const adWithWrapper = { ...ad, nextWrapperURL: wrapperBVastUrl }; jest .spyOn(fetcher, 'fetchVAST') .mockReturnValue(Promise.resolve(expect.any(Object))); VastParser.fetchingCallback = fetcher.fetchVAST; jest .spyOn(VastParser, 'parse') .mockImplementation(() => Promise.resolve([ad])); jest.spyOn(parserUtils, 'mergeWrapperAdData'); VastParser.maxWrapperDepth = 10; return VastParser.resolveWrappers(adWithWrapper, 0, wrapperAVastUrl).then( (res) => { expect(fetcher.fetchVAST).toHaveBeenCalledWith({ url: wrapperBVastUrl, maxWrapperDepth: VastParser.maxWrapperDepth, emitter: expect.any(Function), wrapperDepth: 1, previousUrl: wrapperAVastUrl, wrapperAd: expect.any(Object), }); expect(VastParser.parse).toHaveBeenCalledWith(expect.any(Object), { url: wrapperBVastUrl, previousUrl: wrapperAVastUrl, wrapperSequence: 1, wrapperDepth: 1, }); expect(parserUtils.mergeWrapperAdData).toBeCalled(); expect(res).toHaveLength(1); } ); }); it('will pass timeout error to ad if fetching next wrapper fails', () => { const adWithWrapper = { ...ad, nextWrapperURL: wrapperBVastUrl }; jest.spyOn(fetcher, 'fetchVAST').mockImplementation(() => { return Promise.reject(new Error('timeout')); }); VastParser.fetchingCallback = fetcher.fetchVAST; jest.spyOn(VastParser, 'parse'); jest.spyOn(parserUtils, 'mergeWrapperAdData'); VastParser.maxWrapperDepth = 10; return VastParser.resolveWrappers(adWithWrapper, 0, wrapperAVastUrl).then( (res) => { expect(fetcher.fetchVAST).toHaveBeenCalledWith({ url: wrapperBVastUrl, maxWrapperDepth: VastParser.maxWrapperDepth, emitter: expect.any(Function), wrapperDepth: 1, previousUrl: wrapperAVastUrl, wrapperAd: expect.any(Object), }); expect(VastParser.parse).not.toHaveBeenCalled(); expect(parserUtils.mergeWrapperAdData).not.toBeCalled(); expect(res).toEqual( expect.objectContaining({ errorCode: 301, errorMessage: 'timeout', }) ); } ); }); it('will take the allowMultipleAds value from the option', () => { jest .spyOn(fetcher, 'fetchVAST') .mockReturnValue(Promise.resolve('<xml></xml>')); VastParser.fetchingCallback = fetcher.fetchVAST; jest.spyOn(VastParser, 'parse').mockReturnValue(Promise.resolve()); const adWithWrapper = { ...ad, nextWrapperURL: wrapperBVastUrl, allowMultipleAds: false, }; const expectedValue = { allowMultipleAds: true }; VastParser.initParsingStatus(expectedValue); return VastParser.resolveWrappers(adWithWrapper, 0).then(() => { expect(VastParser.parse).toHaveBeenCalledWith( expect.anything(), expect.objectContaining(expectedValue) ); }); }); it('will take the allowMultipleAds value from the ad if does not set in the option', () => { jest .spyOn(fetcher, 'fetchVAST') .mockReturnValue(Promise.resolve('<xml></xml>')); VastParser.fetchingCallback = fetcher.fetchVAST; jest.spyOn(VastParser, 'parse').mockReturnValue(Promise.resolve()); const expectedValue = { allowMultipleAds: true }; const adWithWrapper = { ...ad, nextWrapperURL: wrapperBVastUrl, ...expectedValue, }; VastParser.initParsingStatus(); return VastParser.resolveWrappers(adWithWrapper, 0).then(() => { expect(VastParser.parse).toHaveBeenCalledWith( expect.anything(), expect.objectContaining(expectedValue) ); }); }); }); describe('getEstimatedBitrate', () => { it('should return value from imported estimatedBitrate', () => { Bitrate.estimatedBitrate = 42; expect(VastParser.getEstimatedBitrate()).toEqual(42); }); }); describe('keepFailedAdPod option', () => { describe('when keepFailedAdPod is false (default)', () => { it('should remove ads with errors from the response', async () => { fetcher.setOptions({ urlHandler: nodeUrlHandler }); VastParser = new VASTParser({ fetcher }); const parser = new DOMParser(); const adWithSequence = parser.parseFromString( `<VAST version="4.3"> <Ad sequence="1"> <InLine> <AdSystem>Test</AdSystem> <AdTitle>Failed Ad</AdTitle> <Creatives> </Creatives> </InLine> </Ad> </VAST>`, 'text/xml' ); const response = await VastParser.parseVAST(adWithSequence, { keepFailedAdPod: false }); expect(response.ads.length).toBe(0); }); it('should remove ads that failed to unwrap', async () => { fetcher.setOptions({ urlHandler: nodeUrlHandler }); VastParser = new VASTParser({ fetcher }); const wrapperFailXml = await nodeUrlHandler.get( './spec/samples/wrapper-empty-no-creative.xml' ); const response = await VastParser.parseVAST(wrapperFailXml.xml); expect(response.ads.length).toBe(0); }); }); describe('when keepFailedAdPod is true', () => { it('should keep ads with errors that have a sequence (ad pod)', async () => { fetcher.setOptions({ urlHandler: nodeUrlHandler }); VastParser = new VASTParser({ fetcher }); const parser = new DOMParser(); const adPodWithFailure = parser.parseFromString( `<VAST version="4.3"> <Ad sequence="1"> <InLine> <AdSystem>Test</AdSystem> <AdTitle>Failed Ad in Pod</AdTitle> <Creatives> </Creatives> </InLine> </Ad> </VAST>`, 'text/xml' ); const response = await VastParser.parseVAST(adPodWithFailure, { keepFailedAdPod: true }); expect(response.ads.length).toBe(1); const ad = response.ads[0]; expect(ad.hasFailed).toBe(true); expect(ad.sequence).toBe('1'); const hasValidCreatives = ad.creatives.some( (creative) => creative.mediaFiles?.length > 0 || creative.variations?.length > 0 ); expect(hasValidCreatives).toBe(false); }); it('should remove standalone ads without sequence even when keepFailedAdPod is true', async () => { fetcher.setOptions({ urlHandler: nodeUrlHandler }); VastParser = new VASTParser({ fetcher }); const parser = new DOMParser(); const standaloneFailedAd = parser.parseFromString( `<VAST version="4.3"> <Ad> <InLine> <AdSystem>Test</AdSystem> <AdTitle>Failed Standalone Ad</AdTitle> <Creatives> </Creatives> </InLine> </Ad> </VAST>`, 'text/xml' ); const response = await VastParser.parseVAST(standaloneFailedAd, { keepFailedAdPod: true }); expect(response.ads.length).toBe(0); }); it('should maintain ad pod sequence with failed ads', async () => { const parser = new DOMParser(); const adPodWithFailureXml = parser.parseFromString( `<VAST version="4.3"> <Ad sequence="1"> <InLine> <AdSystem>Test</AdSystem> <AdTitle>Ad 1</AdTitle> <Creatives> <Creative> <Linear> <Duration>00:00:15</Duration> <MediaFiles> <MediaFile delivery="progressive" type="video/mp4" width="1280" height="720"> <![CDATA[http://example.com/video.mp4]]> </MediaFile> </MediaFiles> </Linear> </Creative> </Creatives> </InLine> </Ad> <Ad sequence="2"> <InLine> <AdSystem>Test</AdSystem> <AdTitle>Ad 2 - Failed</AdTitle> <Creatives> </Creatives> </InLine> </Ad> <Ad sequence="3"> <InLine> <AdSystem>Test</AdSystem> <AdTitle>Ad 3</AdTitle> <Creatives> <Creative> <Linear> <Duration>00:00:15</Duration> <MediaFiles> <MediaFile delivery="progressive" type="video/mp4" width="1280" height="720"> <![CDATA[http://example.com/video2.mp4]]> </MediaFile> </MediaFiles> </Linear> </Creative> </Creatives> </InLine> </Ad> </VAST>`, 'text/xml' ); fetcher.setOptions({ urlHandler: nodeUrlHandler }); VastParser = new VASTParser({ fetcher }); const responseWithKeepFailed = await VastParser.parseVAST( adPodWithFailureXml, { keepFailedAdPod: true } ); expect(responseWithKeepFailed.ads.length).toBe(3); expect(responseWithKeepFailed.ads[0].sequence).toBe('1'); expect(responseWithKeepFailed.ads[1].sequence).toBe('2'); expect(responseWithKeepFailed.ads[1].hasFailed).toBe(true); expect(responseWithKeepFailed.ads[2].sequence).toBe('3'); const responseWithoutKeepFailed = await VastParser.parseVAST( adPodWithFailureXml, { keepFailedAdPod: false } ); expect(responseWithoutKeepFailed.ads.length).toBe(2); }); }); }); });