prebid-universal-creative 1.16.0 → 1.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/.circleci/config.yml +44 -30
  2. package/.github/workflows/codeql.yml +98 -0
  3. package/README.md +2 -2
  4. package/dist/amp.js +3 -3
  5. package/dist/banner.js +3 -3
  6. package/dist/caf7688498213fb0c19f.max.js +1046 -0
  7. package/dist/creative.js +3 -3
  8. package/dist/load-cookie-with-consent.html +1 -1
  9. package/dist/load-cookie.html +1 -1
  10. package/dist/mobile.js +3 -3
  11. package/dist/native-render.js +3 -3
  12. package/dist/native-trk.js +3 -3
  13. package/dist/native.js +3 -3
  14. package/dist/uid.js +2 -2
  15. package/dist/video.js +3 -3
  16. package/gulpfile.js +12 -24
  17. package/integ-test/fixtures/test.js +79 -0
  18. package/integ-test/pages/amp.html +80 -0
  19. package/integ-test/pages/banner.html +96 -0
  20. package/integ-test/pages/native_legacy.html +107 -0
  21. package/integ-test/spec/amp_spec.js +111 -0
  22. package/integ-test/spec/banner_spec.js +85 -0
  23. package/integ-test/spec/native_legacy_spec.js +213 -0
  24. package/package.json +7 -13
  25. package/playwright.config.js +108 -0
  26. package/src/adHtmlRender.js +11 -0
  27. package/src/cookieSync.js +3 -0
  28. package/src/cookieSyncWithConsent.js +3 -0
  29. package/src/domHelper.js +25 -15
  30. package/src/dynamicRenderer.js +56 -0
  31. package/src/messaging.js +23 -2
  32. package/src/mobileAndAmpRender.js +17 -20
  33. package/src/nativeAssetManager.js +98 -79
  34. package/src/nativeORTBTrackerManager.js +1 -1
  35. package/src/nativeRenderManager.js +7 -12
  36. package/src/nativeTrackerManager.js +2 -2
  37. package/src/renderingManager.js +13 -19
  38. package/test/helpers/mocks.js +1 -0
  39. package/test/spec/dynamicRenderer_spec.js +167 -0
  40. package/test/spec/messaging_spec.js +98 -3
  41. package/test/spec/mobileAndAmpRender_spec.js +47 -65
  42. package/test/spec/nativeAssetManager_spec.js +73 -23
  43. package/test/spec/renderingManager_spec.js +20 -6
  44. package/webpack.conf.js +0 -1
  45. package/.nvmrc +0 -1
  46. package/dist/creative.max.js +0 -3102
  47. package/src/postscribeRender.js +0 -10
  48. package/test/e2e/specs/hello_world_banner_non_sf.spec.js +0 -14
  49. package/test/e2e/specs/outstream_non_sf.spec.js +0 -14
  50. package/test/e2e/specs/outstream_sf.spec.js +0 -14
  51. package/wdio.conf.js +0 -50
@@ -7,6 +7,7 @@ import { addNativeClickTrackers, fireNativeImpressionTrackers } from './nativeOR
7
7
  import { sendRequest, loadScript } from './utils';
8
8
  import {prebidMessenger} from './messaging.js';
9
9
  import { isSafeFrame } from './environment.js';
10
+ import {hasDynamicRenderer, runDynamicRenderer} from './dynamicRenderer.js';
10
11
  /*
11
12
  * Native asset->key mapping from Prebid.js/src/constants.json
12
13
  * https://github.com/prebid/Prebid.js/blob/8635c91942de9df4ec236672c39b19448545a812/src/constants.json#L67
@@ -255,7 +256,7 @@ export function newNativeAssetManager(win, nativeTag, mkMessenger = prebidMessen
255
256
  assets,
256
257
  };
257
258
 
258
- return sendMessage(message, replaceAssets);
259
+ return sendMessage(message, replaceAssets(adId));
259
260
  }
260
261
 
261
262
  /*
@@ -268,7 +269,7 @@ export function newNativeAssetManager(win, nativeTag, mkMessenger = prebidMessen
268
269
  action: 'allAssetRequest',
269
270
  adId,
270
271
  };
271
- return sendMessage(message, replaceAssets);
272
+ return sendMessage(message, replaceAssets(adId));
272
273
  }
273
274
 
274
275
  /*
@@ -285,116 +286,131 @@ export function newNativeAssetManager(win, nativeTag, mkMessenger = prebidMessen
285
286
  sendMessage(message);
286
287
  }
287
288
 
288
- /*
289
- * Postmessage listener for when Prebid responds with requested native assets.
290
- */
291
- function replaceAssets(event) {
292
- try {
293
-
294
- var data = {};
295
289
 
290
+ function replaceAssets(adId) {
291
+ return function(event) {
296
292
  try {
297
- data = JSON.parse(event.data);
298
- } catch (e) {
299
- if (errorCountEscapeHatch++ > 10) {
300
- // TODO: this should be a timeout, not an arbitrary cap on the number of messages received
301
- /*
302
- * if for some reason Prebid never responds with the native assets,
303
- * get rid of this listener because other messages won't stop coming
304
- */
305
- stopListening();
306
- throw e;
307
- }
308
- return;
309
- }
310
293
 
311
- if (data.message === 'assetResponse') {
312
- // add GAM %%CLICK_URL_UNESC%% to the data object to be eventually used in renderers
313
- data.clickUrlUnesc = clickUrlUnesc;
314
- const body = win.document.body.innerHTML;
315
- const head = win.document.head.innerHTML;
316
-
317
- if (hasPbNativeData() && data.adId !== win.pbNativeData.adId) return;
294
+ var data = {};
295
+
296
+ try {
297
+ data = JSON.parse(event.data);
298
+ } catch (e) {
299
+ if (errorCountEscapeHatch++ > 10) {
300
+ // TODO: this should be a timeout, not an arbitrary cap on the number of messages received
301
+ /*
302
+ * if for some reason Prebid never responds with the native assets,
303
+ * get rid of this listener because other messages won't stop coming
304
+ */
305
+ stopListening();
306
+ throw e;
307
+ }
308
+ return;
309
+ }
318
310
 
319
- callback = ((cb) => {
320
- return () => {
321
- fireNativeImpressionTrackers(data.adId, sendMessage);
322
- addNativeClickTrackers(data.adId, sendMessage);
323
- cb && cb();
311
+ if (data.message === 'assetResponse' && data.adId === adId) {
312
+ if(hasDynamicRenderer(data)) {
313
+ runDynamicRenderer(adId, data, sendMessage, win);
314
+ return;
324
315
  }
325
- })(callback);
326
316
 
327
- if (head) win.document.head.innerHTML = replace(head, data);
317
+ // add GAM %%CLICK_URL_UNESC%% to the data object to be eventually used in renderers
318
+ data.clickUrlUnesc = clickUrlUnesc;
319
+ const body = win.document.body.innerHTML;
320
+ const head = win.document.head.innerHTML;
328
321
 
322
+ callback = ((cb) => {
323
+ return () => {
324
+ fireNativeImpressionTrackers(data.adId, sendMessage);
325
+ addNativeClickTrackers(data.adId, sendMessage);
326
+ cb && cb();
327
+ }
328
+ })(callback);
329
329
 
330
- data.assets = data.assets || [];
331
- let renderPayload = data.assets;
332
- if (data.ortb) {
333
- renderPayload.ortb = data.ortb;
334
- }
330
+ if (head) win.document.head.innerHTML = replace(head, data);
335
331
 
336
- if ((data.hasOwnProperty('rendererUrl') && data.rendererUrl) || (hasPbNativeData() && win.pbNativeData.hasOwnProperty('rendererUrl'))) {
337
- if (win.renderAd) {
338
- const newHtml = (win.renderAd && win.renderAd(renderPayload)) || '';
339
332
 
340
- renderAd(newHtml, data);
341
- } else if (document.getElementById('pb-native-renderer')) {
342
- document.getElementById('pb-native-renderer').addEventListener('load', function () {
343
- const newHtml = (win.renderAd && win.renderAd(renderPayload)) || '';
333
+ data.assets = data.assets || [];
334
+ let renderPayload = data.assets;
335
+ if (data.ortb) {
336
+ renderPayload.ortb = data.ortb;
337
+ }
344
338
 
345
- renderAd(newHtml, data);
346
- });
347
- } else {
348
- loadScript(win, ((hasPbNativeData() && win.pbNativeData.hasOwnProperty('rendererUrl') && win.pbNativeData.rendererUrl) || data.rendererUrl), function () {
339
+ // if there's a rendererUrl, we need to check whether it's already been loaded.
340
+ // There are 3 scenarios:
341
+ // 1) it's already been loaded (window.renderAd is present)
342
+ // 2) it is currently being loaded (through a script tag with id "pb-native-renderer")
343
+ // 3) it hasn't been loaded yet
344
+ // 1 and 2 seem intended to work with logic in nativeRenderManager.js, which (sometimes) loads rendererUrl through a <script id="pb-native-renderer">, but they could conceivably be used in an undocumented way to embed renderer logic directly in the creative.
345
+ if ((data.hasOwnProperty('rendererUrl') && data.rendererUrl) || (hasPbNativeData() && win.pbNativeData.hasOwnProperty('rendererUrl'))) {
346
+ if (win.renderAd) {
349
347
  const newHtml = (win.renderAd && win.renderAd(renderPayload)) || '';
350
348
 
351
349
  renderAd(newHtml, data);
352
- });
353
- }
354
- } else if ((data.hasOwnProperty('adTemplate') && data.adTemplate) || (hasPbNativeData() && win.pbNativeData.hasOwnProperty('adTemplate'))) {
355
- const template = (hasPbNativeData() && win.pbNativeData.hasOwnProperty('adTemplate') && win.pbNativeData.adTemplate) || data.adTemplate;
356
- const newHtml = replace(template, data);
350
+ } else if (document.getElementById('pb-native-renderer')) {
351
+ document.getElementById('pb-native-renderer').addEventListener('load', function () {
352
+ const newHtml = (win.renderAd && win.renderAd(renderPayload)) || '';
353
+
354
+ renderAd(newHtml, data);
355
+ });
356
+ } else {
357
+ loadScript(win, ((hasPbNativeData() && win.pbNativeData.hasOwnProperty('rendererUrl') && win.pbNativeData.rendererUrl) || data.rendererUrl), function () {
358
+ const newHtml = (win.renderAd && win.renderAd(renderPayload)) || '';
359
+
360
+ renderAd(newHtml, data);
361
+ });
362
+ }
363
+ } else if ((data.hasOwnProperty('adTemplate') && data.adTemplate) || (hasPbNativeData() && win.pbNativeData.hasOwnProperty('adTemplate'))) {
364
+ const template = (hasPbNativeData() && win.pbNativeData.hasOwnProperty('adTemplate') && win.pbNativeData.adTemplate) || data.adTemplate;
365
+ const newHtml = replace(template, data);
357
366
 
358
- renderAd(newHtml, data);
359
- } else {
360
- const newHtml = replace(body, data);
367
+ renderAd(newHtml, data);
368
+ } else {
369
+ const newHtml = replace(body, data);
361
370
 
362
- win.document.body.innerHTML = newHtml;
363
- callback && callback();
364
- stopListening();
371
+ win.document.body.innerHTML = newHtml;
372
+ callback && callback(); // all the other scenarios hit the callback via renderAd()
373
+ stopListening();
374
+ }
365
375
  }
376
+ } catch (e) {
377
+ errCallback && errCallback(e);
366
378
  }
367
- } catch (e) {
368
- errCallback && errCallback(e);
369
379
  }
370
380
  }
371
381
 
372
382
  /** This function returns the element that contains the current iframe. */
373
383
  function getCurrentFrameContainer(win) {
374
- let currentWindow = win;
375
- let currentParentWindow;
384
+ try {
385
+ let currentWindow = win;
386
+ let currentParentWindow;
376
387
 
377
- while (currentWindow !== win.top) {
388
+ while (currentWindow !== win.top) {
378
389
  currentParentWindow = currentWindow.parent;
379
390
  if (!currentParentWindow.frames || !currentParentWindow.frames.length) return null;
380
391
  for (let idx = 0; idx < currentParentWindow.frames.length; idx++)
381
- if (currentParentWindow.frames[idx] === currentWindow) {
382
- if (!currentParentWindow.document) return null;
383
- for (let frameElement of currentParentWindow.document.getElementsByTagName('iframe')) {
384
- if (!frameElement.contentWindow) return null;
385
- if (frameElement.contentWindow === currentWindow) {
386
- return frameElement.parentElement;
387
- }
388
- }
392
+ if (currentParentWindow.frames[idx] === currentWindow) {
393
+ if (!currentParentWindow.document) return null;
394
+ for (let frameElement of currentParentWindow.document.getElementsByTagName('iframe')) {
395
+ if (!frameElement.contentWindow) return null;
396
+ if (frameElement.contentWindow === currentWindow) {
397
+ return frameElement.parentElement;
398
+ }
389
399
  }
400
+ }
401
+ }
402
+ } catch (e) {
403
+ // parent is cross-frame
390
404
  }
391
- }
405
+ }
392
406
 
393
407
  function renderAd(html, bid) {
394
408
  // if the current iframe is not a safeframe, try to set the
395
409
  // current iframe width to the width of the container. This
396
410
  // is to handle the case where the native ad is rendered inside
397
411
  // a GAM display ad.
412
+
413
+ // NOTE: this may be unnecessary, see https://github.com/prebid/prebid-universal-creative/issues/253.
398
414
  if (!isSafeFrame(window)) {
399
415
  let iframeContainer = getCurrentFrameContainer(win);
400
416
  if (iframeContainer && iframeContainer.children && iframeContainer.children[0]) {
@@ -411,9 +427,12 @@ export function newNativeAssetManager(win, nativeTag, mkMessenger = prebidMessen
411
427
 
412
428
  win.document.body.innerHTML += html;
413
429
  callback && callback();
414
- win.removeEventListener('message', replaceAssets);
415
430
  stopListening();
416
- const resize = () => requestHeightResize(bid.adId, (document.body.clientHeight || document.body.offsetHeight), document.body.clientWidth);
431
+ const resize = () => requestHeightResize(
432
+ bid.adId,
433
+ (document.body.clientHeight || document.body.offsetHeight),
434
+ document.body.clientWidth > 1 ? document.body.clientWidth : undefined
435
+ );
417
436
  document.readyState === 'complete' ? resize() : window.onload = resize;
418
437
 
419
438
  if (typeof window.postRenderAd === 'function') {
@@ -21,7 +21,7 @@ export function addNativeClickTrackers(adId, sendMessage) {
21
21
  const adElements = document.getElementsByClassName(AD_ANCHOR_CLASS_NAME) || [];
22
22
  // get all assets that have 'link' property, map asset.id -> asset.link
23
23
  for (let i = 0; i < adElements.length; i++) {
24
- adElements[i].addEventListener('click', (event) => {
24
+ adElements[i].addEventListener('pointerdown', (event) => {
25
25
  let targetElement = event.target;
26
26
  // check if clicked element is associated with any native asset (look for 'hb_native_asset_id' attribute)
27
27
  let assetId = targetElement && targetElement.getAttribute(ASSET_ID_ELEMENT_ATTRIBUTE);
@@ -2,7 +2,7 @@
2
2
  * Script to handle firing impression and click trackers from native teamplates
3
3
  */
4
4
  import {newNativeAssetManager} from './nativeAssetManager';
5
- import {prebidMessenger} from './messaging.js';
5
+ import {prebidMessenger, renderEventMessage} from './messaging.js';
6
6
 
7
7
  export function newNativeRenderManager(win, mkMessenger = prebidMessenger, assetMgr = newNativeAssetManager) {
8
8
  let sendMessage;
@@ -12,13 +12,8 @@ export function newNativeRenderManager(win, mkMessenger = prebidMessenger, asset
12
12
  window.pbNativeData = nativeTag;
13
13
  sendMessage = mkMessenger(nativeTag.pubUrl, win);
14
14
 
15
- function signalResult(adId, success, info) {
16
- sendMessage({
17
- message: 'Prebid Event',
18
- adId,
19
- event: success ? 'adRenderSucceeded' : 'adRenderFailed',
20
- info
21
- });
15
+ function signalResult(adId, errorInfo) {
16
+ sendMessage(renderEventMessage(adId, errorInfo));
22
17
  }
23
18
 
24
19
  try {
@@ -33,16 +28,16 @@ export function newNativeRenderManager(win, mkMessenger = prebidMessenger, asset
33
28
  doc.body.appendChild(scr);
34
29
  }
35
30
  nativeAssetManager.loadAssets(nativeTag.adId, () => {
36
- signalResult(nativeTag.adId, true);
31
+ signalResult(nativeTag.adId);
37
32
  }, (e) => {
38
- signalResult(nativeTag.adId, false, {reason: 'exception', message: e.message});
33
+ signalResult(nativeTag.adId, {reason: 'exception', message: e.message});
39
34
  });
40
35
  } else {
41
- signalResult(null, false, {reason: 'missingDocOrAdid'});
36
+ signalResult(null, {reason: 'missingDocOrAdid'});
42
37
  console.warn('Prebid Native Tag object was missing \'adId\'.');
43
38
  }
44
39
  } catch (e) {
45
- signalResult(nativeTag && nativeTag.adId, false, {reason: 'exception', message: e.message});
40
+ signalResult(nativeTag && nativeTag.adId, {reason: 'exception', message: e.message});
46
41
  console.error('Error rendering ad', e);
47
42
  }
48
43
  };
@@ -47,7 +47,7 @@ export function newNativeTrackerManager(win) {
47
47
 
48
48
  for (let i = 0; i < adElements.length; i++) {
49
49
  let adId = readAdIdFromSingleElement(adElements[i]);
50
- adElements[i].addEventListener('click', function(event) {
50
+ adElements[i].addEventListener('pointerdown', function(event) {
51
51
  listener(event, adId);
52
52
  }, true);
53
53
  }
@@ -83,7 +83,7 @@ export function newNativeTrackerManager(win) {
83
83
  attachClickListeners(false, boundedLoadMobileClickTrackers);
84
84
 
85
85
  (impTrackers || []).forEach(triggerPixel);
86
-
86
+
87
87
  // fire impression IMG trackers
88
88
  eventtrackers
89
89
  .filter(tracker => tracker.event === 1 && tracker.method === 1)
@@ -1,7 +1,8 @@
1
1
  import { parseUrl, transformAuctionTargetingData } from './utils';
2
2
  import { canLocatePrebid } from './environment';
3
3
  import { insertElement, getEmptyIframe } from './domHelper';
4
- import {prebidMessenger} from './messaging.js';
4
+ import {prebidMessenger, renderEventMessage} from './messaging.js';
5
+ import {hasDynamicRenderer, runDynamicRenderer} from './dynamicRenderer.js';
5
6
 
6
7
  export function renderBannerOrDisplayAd(doc, dataObject) {
7
8
  const targetingData = transformAuctionTargetingData(dataObject);
@@ -25,8 +26,8 @@ export function renderLegacy(doc, adId) {
25
26
  w = w.parent;
26
27
  if (w.$$PREBID_GLOBAL$$) {
27
28
  try {
28
- found = true;
29
29
  w.$$PREBID_GLOBAL$$.renderAd(doc, adId);
30
+ found = true;
30
31
  break;
31
32
  } catch (e) {
32
33
  continue;
@@ -49,6 +50,7 @@ export function renderCrossDomain(win, adId, pubAdServerDomain = '', pubUrl) {
49
50
  let adServerDomain = pubAdServerDomain || win.location.hostname;
50
51
  let fullAdServerDomain = windowLocation.protocol + '//' + adServerDomain;
51
52
  const sendMessage = prebidMessenger(pubUrl, win);
53
+ const signalRenderResult = (errorInfo) => sendMessage(renderEventMessage(adId, errorInfo));
52
54
 
53
55
  function renderAd(ev) {
54
56
  let key = ev.message ? "message" : "data";
@@ -64,6 +66,10 @@ export function renderCrossDomain(win, adId, pubAdServerDomain = '', pubUrl) {
64
66
  adObject.message === "Prebid Response" &&
65
67
  adObject.adId === adId
66
68
  ) {
69
+ if (hasDynamicRenderer(adObject)) {
70
+ runDynamicRenderer(adId, adObject, sendMessage, win);
71
+ return;
72
+ }
67
73
  try {
68
74
  let body = win.document.body;
69
75
  let ad = adObject.ad;
@@ -72,7 +78,7 @@ export function renderCrossDomain(win, adId, pubAdServerDomain = '', pubUrl) {
72
78
  let height = adObject.height;
73
79
 
74
80
  if (adObject.mediaType === "video") {
75
- signalRenderResult(false, {
81
+ signalRenderResult({
76
82
  reason: "preventWritingOnMainDocument",
77
83
  message: `Cannot render video ad ${adId}`,
78
84
  });
@@ -83,7 +89,7 @@ export function renderCrossDomain(win, adId, pubAdServerDomain = '', pubUrl) {
83
89
  iframe.contentDocument.open();
84
90
  iframe.contentDocument.write(ad);
85
91
  iframe.contentDocument.close();
86
- signalRenderResult(true);
92
+ signalRenderResult();
87
93
  } else if (url) {
88
94
  const iframe = getEmptyIframe(height, width);
89
95
  iframe.style.display = "inline";
@@ -91,9 +97,9 @@ export function renderCrossDomain(win, adId, pubAdServerDomain = '', pubUrl) {
91
97
  iframe.src = url;
92
98
 
93
99
  insertElement(iframe, document, "body");
94
- signalRenderResult(true);
100
+ signalRenderResult();
95
101
  } else {
96
- signalRenderResult(false, {
102
+ signalRenderResult({
97
103
  reason: "noAd",
98
104
  message: `No ad for ${adId}`,
99
105
  });
@@ -102,22 +108,10 @@ export function renderCrossDomain(win, adId, pubAdServerDomain = '', pubUrl) {
102
108
  );
103
109
  }
104
110
  } catch (e) {
105
- signalRenderResult(false, { reason: "exception", message: e.message });
111
+ signalRenderResult({ reason: "exception", message: e.message });
106
112
  console.log(`Error in rendering ad`, e);
107
113
  }
108
114
  }
109
-
110
- function signalRenderResult(success, { reason, message } = {}) {
111
- const payload = {
112
- message: "Prebid Event",
113
- adId,
114
- event: success ? "adRenderSucceeded" : "adRenderFailed",
115
- };
116
- if (!success) {
117
- payload.info = { reason, message };
118
- }
119
- sendMessage(payload);
120
- }
121
115
  }
122
116
 
123
117
  function requestAdFromPrebid() {
@@ -14,6 +14,7 @@ export const mocks = {
14
14
  },
15
15
  parent: {},
16
16
  top: {},
17
+ frames: {},
17
18
  };
18
19
  }
19
20
  }
@@ -0,0 +1,167 @@
1
+ import {makeIframe} from '../../src/domHelper.js';
2
+ import {hasDynamicRenderer, MIN_RENDERER_VERSION, runDynamicRenderer} from '../../src/dynamicRenderer.js';
3
+ import {AD_RENDER_FAILED, AD_RENDER_SUCCEEDED, PREBID_EVENT} from '../../src/messaging.js';
4
+
5
+ describe('hasDynamicRenderer', () => {
6
+ Object.entries({
7
+ 'neither': {},
8
+ 'renderer, but no version': {
9
+ renderer: 'mock-renderer'
10
+ },
11
+ 'renderer, but version is too low': {
12
+ renderer: 'mock-renderer',
13
+ rendererVersion: 1
14
+ },
15
+ }).forEach(([t, data]) => {
16
+ it(`returns false with ${t}`, () => {
17
+ expect(hasDynamicRenderer(data)).to.be.false;
18
+ })
19
+ });
20
+
21
+ it('returns true when both renderer and version are present', () => {
22
+ expect(hasDynamicRenderer({
23
+ renderer: 'mock-renderer',
24
+ rendererVersion: MIN_RENDERER_VERSION
25
+ })).to.be.true;
26
+ })
27
+ })
28
+
29
+ describe('runDynamicRenderer', () => {
30
+ let sendMessage, frame, render;
31
+ const adId = '123';
32
+ beforeEach(() => {
33
+ render = sinon.stub();
34
+ sendMessage = sinon.stub();
35
+ frame = makeIframe(document);
36
+ return new Promise((resolve) => {
37
+ frame.onload = resolve;
38
+ document.body.appendChild(frame);
39
+ }).then(() => {
40
+ frame.contentWindow._render = render;
41
+ });
42
+ });
43
+
44
+ afterEach(() => {
45
+ document.body.removeChild(frame);
46
+ });
47
+
48
+ function runRenderer(data) {
49
+ return runDynamicRenderer(adId, Object.assign({
50
+ renderer: `window.render = window.parent._render`
51
+ }, data), sendMessage, frame.contentWindow).catch(() => null);
52
+ }
53
+
54
+ it('runs renderer', () => {
55
+ const data = {ad: 'markup'};
56
+ return runRenderer(data).then(() => {
57
+ sinon.assert.calledWith(render, sinon.match(data), sinon.match({mkFrame: makeIframe}), frame.contentWindow);
58
+ });
59
+ });
60
+
61
+ Object.entries({
62
+ 'returns': null,
63
+ 'returns a promise that resolves': Promise.resolve()
64
+ }).forEach(([t, ret]) => {
65
+ it(`emits AD_RENDER_SUCCEDED when renderer ${t}`, () => {
66
+ render.callsFake(() => ret);
67
+ return runRenderer().then(() => {
68
+ sinon.assert.calledWith(sendMessage, {
69
+ adId,
70
+ message: PREBID_EVENT,
71
+ event: AD_RENDER_SUCCEEDED
72
+ });
73
+ });
74
+ });
75
+ });
76
+
77
+ describe('emits AD_RENDER_FAILED', () => {
78
+ Object.entries({
79
+ throws: (ret) => {
80
+ throw ret;
81
+ },
82
+ 'returns a promise that rejects': (ret) => Promise.reject(ret)
83
+ }).forEach(([t, transform]) => {
84
+ describe(`when renderer ${t}`, () => {
85
+ Object.entries({
86
+ 'error': {
87
+ ret: new Error('error message'),
88
+ info: {
89
+ reason: 'exception',
90
+ message: 'error message'
91
+ }
92
+ },
93
+ 'error with reason': {
94
+ ret: {
95
+ reason: 'failure',
96
+ message: 'error message'
97
+ },
98
+ info: {
99
+ reason: 'failure',
100
+ message: 'error message'
101
+ }
102
+ }
103
+ }).forEach(([t, {ret, info}]) => {
104
+ it(`an ${t}`, () => {
105
+ render.callsFake(() => transform(ret));
106
+ return runRenderer().then(() => {
107
+ sinon.assert.calledWith(sendMessage, {
108
+ adId,
109
+ message: PREBID_EVENT,
110
+ event: AD_RENDER_FAILED,
111
+ info
112
+ });
113
+ });
114
+ });
115
+ });
116
+ });
117
+ });
118
+ });
119
+
120
+ describe('renderer sendMessage', () => {
121
+ let rndSendMessage;
122
+ beforeEach(() => {
123
+ render.callsFake(() => new Promise())
124
+ return new Promise((resolve) => {
125
+ render.callsFake((_, {sendMessage}) => {
126
+ rndSendMessage = sendMessage;
127
+ resolve();
128
+ });
129
+ runRenderer();
130
+ })
131
+ });
132
+ it('adds message type and adId', () => {
133
+ rndSendMessage('type', {msg: 'data'});
134
+ sinon.assert.calledWith(sendMessage, {
135
+ message: 'type',
136
+ adId,
137
+ msg: 'data'
138
+ });
139
+ });
140
+ it('accepts response listeners', () => {
141
+ const listener = sinon.stub();
142
+ sendMessage.callsFake((_, responseListener) => {
143
+ responseListener('response');
144
+ })
145
+ rndSendMessage('msg', {}, listener);
146
+ sinon.assert.calledWith(listener, 'response');
147
+ });
148
+
149
+ it('emits AD_RENDER_FAILED if listener throws', () => {
150
+ const listener = sinon.stub().callsFake(() => { throw new Error('err') });
151
+ sendMessage.callsFake((_, responseListener) => {
152
+ responseListener && responseListener('response');
153
+ });
154
+ rndSendMessage('msg', {}, listener);
155
+ sinon.assert.calledWith(sendMessage, {
156
+ message: PREBID_EVENT,
157
+ event: AD_RENDER_FAILED,
158
+ adId,
159
+ info: {
160
+ reason: 'exception',
161
+ message: 'err'
162
+ }
163
+ })
164
+ })
165
+ });
166
+
167
+ });