prebid-universal-creative 1.15.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 (56) hide show
  1. package/.circleci/config.yml +44 -30
  2. package/.github/workflows/codeql.yml +98 -0
  3. package/.github/workflows/issue_tracker.yml +32 -16
  4. package/README.md +4 -2
  5. package/dist/amp.js +3 -3
  6. package/dist/banner.js +3 -3
  7. package/dist/caf7688498213fb0c19f.max.js +1046 -0
  8. package/dist/creative.js +3 -3
  9. package/dist/load-cookie-with-consent.html +1 -1
  10. package/dist/load-cookie.html +1 -1
  11. package/dist/mobile.js +3 -3
  12. package/dist/native-render.js +3 -3
  13. package/dist/native-trk.js +3 -3
  14. package/dist/native.js +3 -3
  15. package/dist/uid.js +2 -2
  16. package/dist/video.js +3 -3
  17. package/gulpfile.js +15 -31
  18. package/integ-test/fixtures/test.js +79 -0
  19. package/integ-test/pages/amp.html +80 -0
  20. package/integ-test/pages/banner.html +96 -0
  21. package/integ-test/pages/native_legacy.html +107 -0
  22. package/integ-test/spec/amp_spec.js +111 -0
  23. package/integ-test/spec/banner_spec.js +85 -0
  24. package/integ-test/spec/native_legacy_spec.js +213 -0
  25. package/karma.conf.maker.js +4 -6
  26. package/package.json +10 -16
  27. package/playwright.config.js +108 -0
  28. package/src/adHtmlRender.js +11 -0
  29. package/src/cookieSync.js +3 -0
  30. package/src/cookieSyncWithConsent.js +3 -0
  31. package/src/domHelper.js +25 -15
  32. package/src/dynamicRenderer.js +56 -0
  33. package/src/messaging.js +23 -2
  34. package/src/mobileAndAmpRender.js +17 -20
  35. package/src/nativeAssetManager.js +134 -80
  36. package/src/nativeORTBTrackerManager.js +3 -3
  37. package/src/nativeRenderManager.js +44 -72
  38. package/src/nativeTrackerManager.js +2 -2
  39. package/src/renderingManager.js +17 -18
  40. package/src/utils.js +0 -9
  41. package/test/helpers/mocks.js +1 -0
  42. package/test/spec/dynamicRenderer_spec.js +167 -0
  43. package/test/spec/messaging_spec.js +98 -3
  44. package/test/spec/mobileAndAmpRender_spec.js +53 -63
  45. package/test/spec/nativeAssetManager_spec.js +290 -93
  46. package/test/spec/nativeORTBTrackerManager_spec.js +3 -19
  47. package/test/spec/nativeRenderManager_spec.js +77 -56
  48. package/test/spec/renderingManager_spec.js +20 -6
  49. package/webpack.conf.js +0 -1
  50. package/.nvmrc +0 -1
  51. package/dist/creative.max.js +0 -3101
  52. package/src/postscribeRender.js +0 -8
  53. package/test/e2e/specs/hello_world_banner_non_sf.spec.js +0 -14
  54. package/test/e2e/specs/outstream_non_sf.spec.js +0 -14
  55. package/test/e2e/specs/outstream_sf.spec.js +0 -14
  56. package/wdio.conf.js +0 -50
@@ -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
+ });
@@ -1,5 +1,11 @@
1
1
  import {mocks} from '../helpers/mocks.js';
2
- import {prebidMessenger} from '../../src/messaging.js';
2
+ import {
3
+ AD_RENDER_FAILED,
4
+ AD_RENDER_SUCCEEDED,
5
+ PREBID_EVENT,
6
+ prebidMessenger,
7
+ renderEventMessage
8
+ } from '../../src/messaging.js';
3
9
 
4
10
  describe('prebidMessenger',() => {
5
11
  let win;
@@ -25,7 +31,6 @@ describe('prebidMessenger',() => {
25
31
  describe('when publisher URL is available', () => {
26
32
  const URL = 'https://www.publisher.com/page.html';
27
33
  const ORIGIN = 'https://www.publisher.com'
28
- let sendMessage;
29
34
  let callback, handler;
30
35
 
31
36
  beforeEach(() => {
@@ -33,15 +38,73 @@ describe('prebidMessenger',() => {
33
38
  handler = h;
34
39
  }
35
40
  win.removeEventListener = sinon.spy();
36
- sendMessage = prebidMessenger(URL, win);
37
41
  callback = sinon.spy();
38
42
  })
39
43
 
44
+ function sendMessage(...args) {
45
+ return prebidMessenger(URL, win)(...args);
46
+ }
47
+
40
48
  it('should use origin for postMessage', () => {
41
49
  sendMessage('test');
42
50
  sinon.assert.calledWith(win.parent.postMessage, JSON.stringify('test'), ORIGIN);
43
51
  });
44
52
 
53
+ describe('when window has multiple ancestors', () => {
54
+ let target;
55
+ beforeEach(() => {
56
+ const top = mocks.createFakeWindow('top');
57
+ target = {
58
+ ...win.parent,
59
+ frames: {},
60
+ parent: {
61
+ top,
62
+ frames: {},
63
+ parent: top
64
+ }
65
+ };
66
+ win = {
67
+ top,
68
+ frames: {},
69
+ parent: {
70
+ top,
71
+ frames: {},
72
+ parent: target
73
+ }
74
+ };
75
+ })
76
+ Object.entries({
77
+ throws() { throw new DOMException() },
78
+ 'does not throw'() { return {} }
79
+ }).forEach(([t, getFrames]) => {
80
+ describe(`when ancestor ${t}`, () => {
81
+ beforeEach(() => {
82
+ Object.defineProperty(target.parent.parent, 'frames', {get: getFrames});
83
+ })
84
+ it('should post to first ancestor that has a __pb_locator__ child', () => {
85
+ [target, target.parent].forEach(win => {
86
+ win.frames = {
87
+ __pb_locator__: {}
88
+ };
89
+ })
90
+ sendMessage('test');
91
+ sinon.assert.called(target.postMessage);
92
+ });
93
+ })
94
+ })
95
+ it('should post to immediate parent when no ancestor has __pb_locator__', () => {
96
+ win.parent.postMessage = sinon.spy();
97
+ delete target.postMessage;
98
+ sendMessage('test');
99
+ sinon.assert.called(win.parent.postMessage);
100
+ });
101
+ it('should post to first restricted frame if no __pb_locator__ can be found', () => {
102
+ Object.defineProperty(target, 'frames', {get() { throw new DOMException() }});
103
+ sendMessage('test');
104
+ sinon.assert.called(target.postMessage)
105
+ })
106
+ });
107
+
45
108
  it('should not run callback on response if origin does not mach', ()=> {
46
109
  sendMessage('test', callback);
47
110
  handler({origin: 'different'});
@@ -62,3 +125,35 @@ describe('prebidMessenger',() => {
62
125
 
63
126
  });
64
127
  })
128
+
129
+ describe('renderEventMessage', () => {
130
+ Object.entries({
131
+ 'success': {
132
+ input: {adId: '123'},
133
+ output: {
134
+ event: AD_RENDER_SUCCEEDED
135
+ }
136
+ },
137
+ 'failure': {
138
+ input: {
139
+ adId: '321',
140
+ errorInfo: {
141
+ reason: 'failureReason',
142
+ message: 'error message'
143
+ }
144
+ },
145
+ output: {
146
+ event: AD_RENDER_FAILED,
147
+ info: {
148
+ reason: 'failureReason',
149
+ message: 'error message'
150
+ }
151
+ }
152
+ }
153
+ }).forEach(([t, {input: {adId, errorInfo}, output}]) => {
154
+ Object.assign(output, {message: PREBID_EVENT, adId});
155
+ it(t, () => {
156
+ expect(renderEventMessage(adId, errorInfo)).to.eql(output);
157
+ })
158
+ })
159
+ })
@@ -1,9 +1,10 @@
1
1
  import { renderAmpOrMobileAd } from 'src/mobileAndAmpRender';
2
- import * as postscribeRender from 'src/postscribeRender'
2
+ import * as postscribeRender from 'src/adHtmlRender'
3
3
  import * as utils from 'src/utils';
4
4
  import { expect } from 'chai';
5
5
  import { mocks } from 'test/helpers/mocks';
6
6
  import { merge } from 'lodash';
7
+ import {writeAdHtml} from 'src/adHtmlRender';
7
8
 
8
9
 
9
10
  function renderingMocks() {
@@ -187,51 +188,58 @@ describe("renderingManager", function () {
187
188
  });
188
189
 
189
190
  describe("amp creative", function () {
191
+ let sandbox;
190
192
  let writeHtmlSpy;
191
193
  let sendRequestSpy;
192
194
  let triggerPixelSpy;
193
195
  let mockWin;
194
-
195
- before(function () {
196
- writeHtmlSpy = sinon.spy(postscribeRender, "writeAdHtml");
197
- sendRequestSpy = sinon.spy(utils, "sendRequest");
198
- triggerPixelSpy = sinon.spy(utils, "triggerPixel");
199
- mockWin = merge(
200
- mocks.createFakeWindow("http://example.com"),
201
- renderingMocks().getWindowObject()
202
- );
203
- });
204
-
205
- afterEach(function () {
206
- writeHtmlSpy.resetHistory();
207
- sendRequestSpy.resetHistory();
208
- triggerPixelSpy.resetHistory();
209
- });
210
-
211
- after(function () {
212
- writeHtmlSpy.restore();
213
- sendRequestSpy.restore();
214
- triggerPixelSpy.restore();
215
- });
216
-
217
- it("should render amp creative", function () {
218
- let ucTagData = {
196
+ let ucTagData;
197
+ let response;
198
+
199
+ beforeEach(function () {
200
+ sandbox = sinon.sandbox.create();
201
+ writeHtmlSpy = sandbox.spy(postscribeRender, "writeAdHtml");
202
+ sendRequestSpy = sandbox.spy(utils, "sendRequest");
203
+ triggerPixelSpy = sandbox.spy(utils, "triggerPixel");
204
+ ucTagData = {
219
205
  cacheHost: "example.com",
220
206
  cachePath: "/path",
221
207
  uuid: "123",
222
208
  size: "300x250",
223
- hbPb: "10.00",
224
209
  };
225
-
226
- renderAmpOrMobileAd(ucTagData);
227
-
228
- let response = {
210
+ response = {
229
211
  width: 300,
230
212
  height: 250,
231
213
  crid: 123,
232
214
  adm: "ad-markup${AUCTION_PRICE}",
233
215
  wurl: "https://test.prebidcache.wurl",
234
216
  };
217
+ });
218
+
219
+
220
+
221
+ afterEach(function () {
222
+ sandbox.restore();
223
+ });
224
+
225
+ it('should send embed-resize message', () => {
226
+ sandbox.spy(window.parent, 'postMessage');
227
+ ucTagData.size = '400x500'
228
+ renderAmpOrMobileAd(ucTagData);
229
+ requests[0].respond(200, {}, JSON.stringify(response));
230
+ sinon.assert.calledWith(window.parent.postMessage, {
231
+ sentinel: "amp",
232
+ type: "embed-size",
233
+ width: 400,
234
+ height: 500,
235
+ });
236
+ })
237
+
238
+ it("should render amp creative", function () {
239
+ ucTagData.hbPb = "10.00";
240
+ renderAmpOrMobileAd(ucTagData);
241
+
242
+
235
243
  requests[0].respond(200, {}, JSON.stringify(response));
236
244
  expect(writeHtmlSpy.args[0][0]).to.equal(
237
245
  "<!--Creative 123 served by Prebid.js Header Bidding-->ad-markup10.00"
@@ -245,24 +253,8 @@ describe("renderingManager", function () {
245
253
  });
246
254
 
247
255
  it("should replace AUCTION_PRICE with response.price over hbPb", function () {
248
- let ucTagData = {
249
- cacheHost: "example.com",
250
- cachePath: "/path",
251
- uuid: "123",
252
- size: "300x250",
253
- hbPb: "10.00",
254
- };
255
-
256
256
  renderAmpOrMobileAd(ucTagData);
257
-
258
- let response = {
259
- width: 300,
260
- height: 250,
261
- crid: 123,
262
- price: 12.5,
263
- adm: "ad-markup${AUCTION_PRICE}",
264
- wurl: "https://test.prebidcache.wurl",
265
- };
257
+ response.price = 12.5;
266
258
  requests[0].respond(200, {}, JSON.stringify(response));
267
259
  expect(writeHtmlSpy.args[0][0]).to.equal(
268
260
  "<!--Creative 123 served by Prebid.js Header Bidding-->ad-markup12.5"
@@ -276,22 +268,7 @@ describe("renderingManager", function () {
276
268
  });
277
269
 
278
270
  it("should replace AUCTION_PRICE with with empty value when neither price nor hbPb exist", function () {
279
- let ucTagData = {
280
- cacheHost: "example.com",
281
- cachePath: "/path",
282
- uuid: "123",
283
- size: "300x250",
284
- };
285
-
286
271
  renderAmpOrMobileAd(ucTagData);
287
-
288
- let response = {
289
- width: 300,
290
- height: 250,
291
- crid: 123,
292
- adm: "ad-markup${AUCTION_PRICE}",
293
- wurl: "https://test.prebidcache.wurl",
294
- };
295
272
  requests[0].respond(200, {}, JSON.stringify(response));
296
273
  expect(writeHtmlSpy.args[0][0]).to.equal(
297
274
  "<!--Creative 123 served by Prebid.js Header Bidding-->ad-markup"
@@ -306,3 +283,16 @@ describe("renderingManager", function () {
306
283
  });
307
284
  });
308
285
 
286
+ describe('writeAdHtml', () => {
287
+ it('removes DOCTYPE from markup', () => {
288
+ const ps = sinon.stub();
289
+ writeAdHtml('<!DOCTYPE html><div>mock-ad</div>', ps);
290
+ sinon.assert.calledWith(ps, sinon.match.any, '<div>mock-ad</div>')
291
+ });
292
+
293
+ it('removes lowercase doctype from markup', () => {
294
+ const ps = sinon.stub();
295
+ writeAdHtml('<!doctype html><div>mock-ad</div>', ps);
296
+ sinon.assert.calledWith(ps, sinon.match.any, '<div>mock-ad</div>')
297
+ });
298
+ })