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
@@ -0,0 +1,213 @@
1
+ import {test, expect} from '../fixtures/test.js';
2
+
3
+ test.describe('Legacy native', () => {
4
+
5
+ const TRACKER_URL = 'https://www.tracker.com/';
6
+ const TRACKERS = Object.fromEntries(
7
+ ['imp', 'js', 'click'].map(ttype => [ttype, `${TRACKER_URL}${ttype}`])
8
+ )
9
+
10
+ const TEMPLATE = `
11
+ <div id="the-ad">
12
+ <a class="clickUrl" href="##hb_native_linkurl##">Click</a>
13
+ <img class="image" width="100" src="##hb_native_image##" />
14
+ <div class="title pb-click">##hb_native_title##</div>
15
+ <div class="body">##hb_native_body##</div>
16
+ </div>
17
+ `;
18
+
19
+ const RENDERER_URL = 'https://www.custom-renderer.com/renderer.js';
20
+
21
+ function customRenderer(data) {
22
+ const assets = Object.fromEntries(data.map((d) => [d.key, d.value]))
23
+ return `
24
+ <div id="the-ad">
25
+ <a class="clickUrl" href="${assets.clickUrl}">Click</a>
26
+ <img class="image" width="100" src="${assets.image}" />
27
+ <div class="title pb-click">${assets.title}</div>
28
+ <div class="body">${assets.body}</div>
29
+ </div>
30
+ `
31
+ }
32
+
33
+
34
+ const ASSETS = {
35
+ image: {
36
+ value: 'https://prebid.org/wp-content/uploads/2021/02/Prebid-Logo-RGB-Full-Color-Medium.svg',
37
+ expect(e) {
38
+ return e.toHaveAttribute('src', this.value)
39
+ }
40
+ },
41
+ title: {
42
+ value: 'Ad title',
43
+ expect(e) {
44
+ return e.toHaveText(this.value)
45
+ }
46
+ },
47
+ body: {
48
+ value: 'Ad body',
49
+ expect(e) {
50
+ return e.toHaveText(this.value)
51
+ }
52
+ },
53
+ clickUrl: {
54
+ value: 'https://some-link.com',
55
+ expect(e) {
56
+ return e.toHaveAttribute('href', this.value)
57
+ }
58
+ }
59
+ }
60
+
61
+ let trackersFired;
62
+
63
+ test.beforeEach(async ({page}) => {
64
+ trackersFired = {};
65
+ await page.route((u) => u.href.startsWith(TRACKER_URL), async (route, req) => {
66
+ const ttype = req.url().substring(TRACKER_URL.length);
67
+ trackersFired[ttype] = true;
68
+ await route.fulfill({});
69
+ });
70
+ await page.route((u) => u.href.startsWith(RENDERER_URL), async (route) => {
71
+ await route.fulfill({
72
+ contentType: 'application/javascript',
73
+ body: `window.renderAd = ${customRenderer.toString()};`
74
+ })
75
+ })
76
+ });
77
+
78
+
79
+ function bidResponse(native) {
80
+ return {
81
+ ad: null,
82
+ adId: 'mock-ad',
83
+ native
84
+ }
85
+ }
86
+
87
+ function getCreative(isBanner, isTemplateInCreative, isSafeFrame) {
88
+ if (!isBanner) {
89
+ return isTemplateInCreative ? 'native-legacy' : 'native-no-template'
90
+ } else {
91
+ return (isTemplateInCreative ? 'native-banner-legacy' : 'native-banner-no-template') + (isSafeFrame ? '' : '-no-frame');
92
+ }
93
+ }
94
+
95
+
96
+ Object.entries({
97
+ 'legacy response': {
98
+ ...Object.fromEntries(Object.entries(ASSETS).map(([name, asset]) => [name, asset.value])),
99
+ javascriptTrackers: [`<script src="${TRACKERS.js}"></script>`],
100
+ impressionTrackers: [TRACKERS.imp],
101
+ clickTrackers: [TRACKERS.click],
102
+ },
103
+ 'ortb response': {
104
+ ortb: {
105
+ ver: '1.2',
106
+ link: {
107
+ url: ASSETS.clickUrl.value,
108
+ clicktrackers: [
109
+ TRACKERS.click
110
+ ],
111
+ },
112
+ jstracker: `<script src="${TRACKERS.js}"></script>`,
113
+ eventtrackers: [
114
+ {
115
+ url: TRACKERS.imp,
116
+ event: 1,
117
+ method: 1
118
+ }
119
+ ],
120
+ assets: [
121
+ {
122
+ id: 0,
123
+ img: {
124
+ url: ASSETS.image.value,
125
+ }
126
+ },
127
+ {
128
+ id: 1,
129
+ title: {
130
+ text: ASSETS.title.value
131
+ }
132
+ },
133
+ {
134
+ id: 2,
135
+ data: {
136
+ value: ASSETS.body.value
137
+ }
138
+ }
139
+ ]
140
+ }
141
+ }
142
+ }).forEach(([t, native]) => {
143
+ test.describe(t, () => {
144
+ Object.entries({
145
+ 'native proper': false,
146
+ 'native in banner': true
147
+ }).forEach(([t, isBanner]) => {
148
+ test.describe(t, () => {
149
+ Object.entries({
150
+ 'template in creative': {
151
+ isTemplateInCreative: true,
152
+ },
153
+ 'adTemplate': {
154
+ adTemplate: TEMPLATE
155
+ },
156
+ 'custom renderer': {
157
+ rendererUrl: RENDERER_URL
158
+ }
159
+ }).forEach(([t, {isTemplateInCreative, adTemplate, rendererUrl}]) => {
160
+ test.describe(t, () => {
161
+ Object.entries({
162
+ 'safeframe': true,
163
+ 'non safeframe': false,
164
+ }).forEach(([t, isSafeFrame]) => {
165
+ if (!isSafeFrame && !isBanner) return; // there's no option to run GAM native ads without safeframe
166
+
167
+ test.describe(t, () => {
168
+ test.beforeEach(async ({page}) => {
169
+ await page.goto(`native_legacy.html?creative=${getCreative(isBanner, isTemplateInCreative, isSafeFrame)}&banner=${isBanner}&bidResponse=${encodeURIComponent(JSON.stringify(bidResponse({...native, adTemplate, rendererUrl})))}`);
170
+ });
171
+
172
+ test('should display ad', async ({crossLocator}) => {
173
+ await expect(await crossLocator('#the-ad')).toBeVisible();
174
+ });
175
+
176
+ test('should fill in assets', async ({crossLocator}) => {
177
+ await Promise.all(Object.entries(ASSETS).map(async ([name, a]) => await a.expect(expect(await crossLocator(`#the-ad .${name}`)))))
178
+ });
179
+
180
+ // TODO: should this emit AD_RENDER_SUCCEEDED? see https://github.com/prebid/prebid-universal-creative/issues/182
181
+ ['bidWon'].forEach(ev => {
182
+ test(`should emit '${ev}'`, async ({expectEvent}) => {
183
+ await expectEvent(event => event.eventType === ev && event.args.adId === 'mock-ad')
184
+ })
185
+ });
186
+
187
+ ['js', 'imp'].forEach(ttype => {
188
+ test(`should fire ${ttype} trackers`, async () => {
189
+ await expect.poll(() => trackersFired[ttype]).toBeTruthy();
190
+ })
191
+ })
192
+
193
+ test('should fire click trackers', async ({crossLocator, browserName}, testInfo) => {
194
+ if (browserName === 'webkit' && testInfo.project.use.headless !== false) {
195
+ // webkit does not like this test. It passes locally in headed mode:
196
+ // $ npx run playwright test --headed --workers 1 --project webkit -g "should fire click trackers"
197
+ // but I am unable to get headed tests to work on the pipeline
198
+ // (e.g. https://app.circleci.com/pipelines/github/prebid/prebid-universal-creative/309/workflows/b9bafe18-e2b3-4081-a2f0-e74b33575b56/jobs/573)
199
+ return;
200
+ }
201
+ const el = await crossLocator('#the-ad .pb-click');
202
+ await el.click();
203
+ await expect.poll(() => trackersFired.click).toBeTruthy();
204
+ });
205
+ });
206
+ });
207
+ });
208
+ });
209
+ });
210
+ });
211
+ });
212
+ });
213
+ });
@@ -3,7 +3,7 @@ const webpackConf = require('./webpack.conf');
3
3
  const karmaConstants = require('karma').constants;
4
4
  const path = require('path');
5
5
 
6
- function setBrowsers(karmaConf, browserstack, watchMode) {
6
+ function setBrowsers(karmaConf, browserstack) {
7
7
  if (browserstack) {
8
8
  karmaConf.browserStack = {
9
9
  username: process.env.BROWSERSTACK_USERNAME,
@@ -12,8 +12,6 @@ function setBrowsers(karmaConf, browserstack, watchMode) {
12
12
  }
13
13
  karmaConf.customLaunchers = require('./browsers.json')
14
14
  karmaConf.browsers = Object.keys(karmaConf.customLaunchers);
15
- } else if (watchMode) {
16
- karmaConf.browsers = ['Chrome'];
17
15
  }
18
16
  }
19
17
 
@@ -29,7 +27,7 @@ function setReporters(karmaConf, codeCoverage, browserstack) {
29
27
  suppressPassed: true
30
28
  };
31
29
  }
32
-
30
+
33
31
  if (codeCoverage) {
34
32
  karmaConf.reporters.push('coverage-istanbul');
35
33
  karmaConf.coverageIstanbulReporter = {
@@ -41,7 +39,7 @@ function setReporters(karmaConf, codeCoverage, browserstack) {
41
39
  urlFriendlyName: true, // simply replaces spaces with _ for files/dirs
42
40
  }
43
41
  }
44
- }
42
+ }
45
43
  }
46
44
  }
47
45
 
@@ -149,6 +147,6 @@ module.exports = function(codeCoverage, browserstack, watchMode) {
149
147
  captureTimeout: 4 * 60 * 1000, // default 60000
150
148
  }
151
149
  setReporters(config, codeCoverage, browserstack);
152
- setBrowsers(config, browserstack, watchMode);
150
+ setBrowsers(config, browserstack);
153
151
  return config;
154
152
  }
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "prebid-universal-creative",
3
- "version": "1.15.0",
3
+ "version": "1.17.0",
4
4
  "description": "Universal creative for Prebid apps",
5
5
  "main": "dist/creative.js",
6
6
  "scripts": {
7
- "test": "echo test",
7
+ "test": "gulp test",
8
8
  "prepublish": "gulp build"
9
9
  },
10
10
  "repository": {
@@ -26,13 +26,7 @@
26
26
  "@babel/plugin-transform-modules-commonjs": "^7.6.0",
27
27
  "@babel/preset-env": "^7.2.3",
28
28
  "@babel/register": "^7.6.2",
29
- "@wdio/browserstack-service": "^7.25.4",
30
- "@wdio/cli": "^7.25.4",
31
- "@wdio/concise-reporter": "^7.25.4",
32
- "@wdio/local-runner": "^7.25.4",
33
- "@wdio/mocha-framework": "^7.25.4",
34
- "@wdio/spec-reporter": "^7.25.4",
35
- "@wdio/sync": "^7.25.4",
29
+ "@playwright/test": "^1.50.1",
36
30
  "babel-loader": "^8.0.5",
37
31
  "babel-plugin-transform-es3-member-expression-literals": "^6.22.0",
38
32
  "babel-plugin-transform-es3-property-literals": "^6.22.0",
@@ -41,7 +35,7 @@
41
35
  "core-js-pure": "^3.13.0",
42
36
  "del": "^5.0.0",
43
37
  "execa": "^1.0.0",
44
- "gulp": "^4.0.2",
38
+ "gulp": "^5.0.0",
45
39
  "gulp-clean": "^0.4.0",
46
40
  "gulp-connect": "^5.7.0",
47
41
  "gulp-eslint": "^4.0.2",
@@ -58,25 +52,25 @@
58
52
  "karma-chrome-launcher": "^2.2.0",
59
53
  "karma-coverage": "^1.1.2",
60
54
  "karma-coverage-istanbul-reporter": "^1.4.3",
61
- "karma-mocha": "^1.3.0",
55
+ "karma-mocha": "^2.0.1",
62
56
  "karma-mocha-reporter": "^2.2.5",
63
57
  "karma-sinon": "^1.0.5",
64
58
  "karma-sourcemap-loader": "^0.3.7",
65
59
  "karma-spec-reporter": "^0.0.31",
66
60
  "karma-webpack": "^3.0.5",
67
61
  "lodash": "^4.17.14",
68
- "mocha": "^5.2.0",
62
+ "mocha": "^10.2.0",
69
63
  "opn": "^6.0.0",
70
64
  "sinon": "^6.3.4",
71
65
  "string-replace-webpack-plugin": "^0.1.3",
72
- "webdriverio": "^7.25.2",
66
+ "webdriverio": "^9.10.0",
73
67
  "webpack": "^3.12.0",
68
+ "webpack-common-shake": "^1.0.0",
74
69
  "webpack-stream": "^4.0.0",
75
- "yargs": "^11.0.0",
76
- "webpack-common-shake": "^1.0.0"
70
+ "yargs": "^11.0.0"
77
71
  },
78
72
  "dependencies": {
79
73
  "babel-plugin-transform-object-assign": "^6.22.0",
80
- "postscribe": "^2.0.8"
74
+ "gulp-cli": "^3.0.0"
81
75
  }
82
76
  }
@@ -0,0 +1,108 @@
1
+ // @ts-check
2
+ const { devices } = require('@playwright/test');
3
+ const {BASE_URL} = require('./integ-test/fixtures/test.js');
4
+
5
+ /**
6
+ * Read environment variables from file.
7
+ * https://github.com/motdotla/dotenv
8
+ */
9
+ // require('dotenv').config();
10
+
11
+
12
+ /**
13
+ * @see https://playwright.dev/docs/test-configuration
14
+ * @type {import('@playwright/test').PlaywrightTestConfig}
15
+ */
16
+ const config = {
17
+ testDir: './integ-test/spec',
18
+ testMatch: /.*.js/,
19
+ /* Maximum time one test can run for. */
20
+ timeout: 30 * 1000,
21
+ expect: {
22
+ /**
23
+ * Maximum time expect() should wait for the condition to be met.
24
+ * For example in `await expect(locator).toHaveText();`
25
+ */
26
+ timeout: 15000
27
+ },
28
+ /* Run tests in files in parallel */
29
+ fullyParallel: true,
30
+ /* Fail the build on CI if you accidentally left test.only in the source code. */
31
+ forbidOnly: !!process.env.CI,
32
+ /* Retry on CI only */
33
+ retries: process.env.CI ? 3 : 0,
34
+ workers: process.env.CI ? 10 : undefined,
35
+ /* Reporter to use. See https://playwright.dev/docs/test-reporters */
36
+ reporter: 'html',
37
+ /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
38
+ use: {
39
+ /* Maximum time each action such as `click()` can take. Defaults to 0 (no limit). */
40
+ actionTimeout: 0,
41
+ /* Base URL to use in actions like `await page.goto('/')`. */
42
+ baseURL: BASE_URL,
43
+
44
+ /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
45
+ trace: 'on-first-retry',
46
+ },
47
+
48
+ /* Configure projects for major browsers */
49
+ projects: [
50
+ {
51
+ name: 'firefox',
52
+ use: {
53
+ ...devices['Desktop Firefox'],
54
+ },
55
+ },
56
+ {
57
+ name: 'chromium',
58
+ use: {
59
+ ...devices['Desktop Chrome'],
60
+ },
61
+ },
62
+
63
+ {
64
+ name: 'webkit',
65
+ use: {
66
+ ...devices['Desktop Safari'],
67
+ },
68
+ },
69
+ /* Test against mobile viewports. */
70
+ // {
71
+ // name: 'Mobile Chrome',
72
+ // use: {
73
+ // ...devices['Pixel 5'],
74
+ // },
75
+ // },
76
+ // {
77
+ // name: 'Mobile Safari',
78
+ // use: {
79
+ // ...devices['iPhone 12'],
80
+ // },
81
+ // },
82
+
83
+ /* Test against branded browsers. */
84
+ // {
85
+ // name: 'Microsoft Edge',
86
+ // use: {
87
+ // channel: 'msedge',
88
+ // },
89
+ // },
90
+ // {
91
+ // name: 'Google Chrome',
92
+ // use: {
93
+ // channel: 'chrome',
94
+ // },
95
+ // },
96
+ ],
97
+
98
+ /* Folder for test artifacts such as screenshots, videos, traces, etc. */
99
+ // outputDir: 'test-results/',
100
+
101
+ /* Run your local dev server before starting the tests */
102
+ // webServer: {
103
+ // command: 'npm run start',
104
+ // port: 3000,
105
+ // },
106
+ };
107
+
108
+ module.exports = config;
@@ -0,0 +1,11 @@
1
+ export function writeAdHtml(markup, insertHTML = document.body.insertAdjacentHTML.bind(document.body)) {
2
+ // remove <?xml> and <!doctype> tags
3
+ // https://github.com/prebid/prebid-universal-creative/issues/134
4
+ markup = markup.replace(/\<(\?xml|(\!DOCTYPE[^\>\[]+(\[[^\]]+)?))+[^>]+\>/gi, '');
5
+
6
+ try {
7
+ insertHTML('beforeend', markup);
8
+ } catch (error) {
9
+ console.error(error);
10
+ }
11
+ }
package/src/cookieSync.js CHANGED
@@ -20,6 +20,8 @@ const BIDDER_ARGS = sanitizeBidders(parseQueryParam('bidders', window.location.s
20
20
  const IS_AMP = sanitizeSource(parseQueryParam('source', window.location.search));
21
21
  const maxSyncCountParam = parseQueryParam('max_sync_count', window.location.search);
22
22
  const MAX_SYNC_COUNT = sanitizeSyncCount(parseInt((maxSyncCountParam) ? maxSyncCountParam : 10, 10));
23
+ const coopSyncParam = parseQueryParam('coop_sync', window.location.search);
24
+ const COOP_SYNC = !coopSyncParam || coopSyncParam === 'true' || !!parseInt(coopSyncParam);
23
25
  const GDPR = sanitizeGdpr(parseInt(parseQueryParam('gdpr', window.location.search), 10));
24
26
  const GDPR_CONSENT = sanitizeGdprConsent(parseQueryParam('gdpr_consent', window.location.search));
25
27
 
@@ -254,6 +256,7 @@ function sanitizeBidders(value) {
254
256
  function getStringifiedData(endPointArgs) {
255
257
  var data = (endPointArgs && typeof endPointArgs === 'object') ? endPointArgs : {}
256
258
  data['limit'] = MAX_SYNC_COUNT;
259
+ data['coopSync'] = COOP_SYNC;
257
260
 
258
261
  if(GDPR) data.gdpr = GDPR;
259
262
  if(GDPR_CONSENT) data.gdpr_consent = GDPR_CONSENT;
@@ -20,6 +20,8 @@ const IS_AMP = sanitizeSource(parseQueryParam('source', window.location.search))
20
20
  const BIDDER_ARGS = sanitizeBidders(parseQueryParam('bidders', window.location.search));
21
21
  const maxSyncCountParam = parseQueryParam('max_sync_count', window.location.search);
22
22
  const MAX_SYNC_COUNT = sanitizeSyncCount(parseInt((maxSyncCountParam) ? maxSyncCountParam : 10, 10));
23
+ const coopSyncParam = parseQueryParam('coop_sync', window.location.search);
24
+ const COOP_SYNC = !coopSyncParam || coopSyncParam === 'true' || !!parseInt(coopSyncParam);
23
25
  const TIMEOUT = sanitizeTimeout(parseInt(parseQueryParam('timeout', window.location.search), 10));
24
26
  const DEFAULT_GDPR_SCOPE = sanitizeScope(parseInt(parseQueryParam('defaultGdprScope', window.location.search), 10));
25
27
 
@@ -273,6 +275,7 @@ function attachConsent(data) {
273
275
  function getStringifiedData(endPointArgs) {
274
276
  var data = (endPointArgs && typeof endPointArgs === 'object') ? endPointArgs : {}
275
277
  data['limit'] = MAX_SYNC_COUNT;
278
+ data['coopSync'] = COOP_SYNC;
276
279
 
277
280
  if(IS_AMP) data.filterSettings = {
278
281
  iframe: {
package/src/domHelper.js CHANGED
@@ -3,25 +3,35 @@
3
3
  */
4
4
 
5
5
 
6
+
7
+ /**
8
+ * returns a empty iframe element with specified attributes.
9
+ */
10
+ export function makeIframe(doc, attrs = {}) {
11
+ const frame = doc.createElement('iframe');
12
+ Object.entries(Object.assign({
13
+ frameborder: 0,
14
+ scrolling: 'no',
15
+ marginheight: 0,
16
+ marginwidth: 0,
17
+ TOPMARGIN: 0,
18
+ LEFTMARGIN: 0,
19
+ allowtransparency: 'true'
20
+ }, attrs)).forEach(([attr, value]) => {
21
+ frame.setAttribute(attr, value);
22
+ });
23
+ return frame;
24
+ }
25
+
6
26
  /**
7
27
  * returns a empty iframe element with specified height/width
8
- * @param {Number} height height iframe set to
28
+ * @param {Number} height height iframe set to
9
29
  * @param {Number} width width iframe set to
10
- * @returns {Element} iframe DOM element
30
+ * @returns {Element} iframe DOM element
11
31
  */
12
32
  export function getEmptyIframe(height, width) {
13
- let frame = document.createElement('iframe');
14
- frame.setAttribute('frameborder', 0);
15
- frame.setAttribute('scrolling', 'no');
16
- frame.setAttribute('marginheight', 0);
17
- frame.setAttribute('marginwidth', 0);
18
- frame.setAttribute('TOPMARGIN', 0);
19
- frame.setAttribute('LEFTMARGIN', 0);
20
- frame.setAttribute('allowtransparency', 'true');
21
- frame.setAttribute('width', width);
22
- frame.setAttribute('height', height);
23
- return frame;
24
- }
33
+ return makeIframe(document, {height, width})
34
+ }
25
35
 
26
36
  /**
27
37
  * Insert element to passed target
@@ -44,4 +54,4 @@ export function insertElement(elm, doc, target) {
44
54
  elToAppend.insertBefore(elm, elToAppend.firstChild);
45
55
  }
46
56
  } catch (e) {}
47
- }
57
+ }
@@ -0,0 +1,56 @@
1
+ import {makeIframe} from './domHelper.js';
2
+ import {renderEventMessage} from './messaging.js';
3
+
4
+ export const MIN_RENDERER_VERSION = 3;
5
+
6
+ export function hasDynamicRenderer(message) {
7
+ return typeof message.renderer === 'string' && parseInt(message.rendererVersion, 10) >= MIN_RENDERER_VERSION
8
+ }
9
+
10
+ export function runDynamicRenderer(adId, data, sendMessage, win = window, mkFrame = makeIframe) {
11
+ const renderer = mkFrame(win.document, {
12
+ width: 0,
13
+ height: 0,
14
+ style: 'display: none',
15
+ srcdoc: `<script>${data.renderer}</script>`,
16
+ name: '__pb_renderer__'
17
+ });
18
+
19
+ return new Promise((resolve, reject) => {
20
+ function onError(e = {}) {
21
+ sendMessage(renderEventMessage(adId, {
22
+ reason: e.reason || 'exception',
23
+ message: e.message
24
+ }));
25
+ e.stack && console.error(e);
26
+ reject(e);
27
+ }
28
+
29
+ function guard(fn) {
30
+ return function () {
31
+ try {
32
+ return fn.apply(this, arguments);
33
+ } catch (e) {
34
+ onError(e);
35
+ }
36
+ };
37
+ }
38
+
39
+ renderer.onload = guard(function () {
40
+ const W = renderer.contentWindow;
41
+ // NOTE: on Firefox, `Promise.resolve(P)` or `new Promise((resolve) => resolve(P))`
42
+ // does not appear to work if P comes from another frame
43
+ W.Promise.resolve(W.render(data, {
44
+ mkFrame,
45
+ sendMessage: (type, payload, onResponse) => sendMessage(
46
+ Object.assign({adId, message: type}, payload),
47
+ onResponse ? guard(onResponse) : undefined
48
+ )
49
+ }, win)).then(
50
+ () => sendMessage(renderEventMessage(adId)),
51
+ onError
52
+ ).then(resolve);
53
+ });
54
+ win.document.body.appendChild(renderer);
55
+ });
56
+ }
package/src/messaging.js CHANGED
@@ -1,4 +1,7 @@
1
1
  import {parseUrl} from './utils.js';
2
+ export const PREBID_EVENT = 'Prebid Event';
3
+ export const AD_RENDER_SUCCEEDED = 'adRenderSucceeded';
4
+ export const AD_RENDER_FAILED = 'adRenderFailed';
2
5
 
3
6
  export function prebidMessenger(publisherURL, win = window) {
4
7
  const prebidDomain = (() => {
@@ -9,6 +12,16 @@ export function prebidMessenger(publisherURL, win = window) {
9
12
  return parsedUrl.protocol + '://' + parsedUrl.host;
10
13
  })();
11
14
 
15
+ function isPrebidWindow(win) {
16
+ return win && win.frames && win.frames.__pb_locator__;
17
+ }
18
+
19
+ let target = win.parent;
20
+ try {
21
+ while (target != null && target !== win.top && !isPrebidWindow(target)) target = target.parent;
22
+ if (!isPrebidWindow(target)) target = win.parent;
23
+ } catch (e) {}
24
+
12
25
  return function sendMessage(message, onResponse) {
13
26
  if (prebidDomain == null) {
14
27
  throw new Error('Missing pubUrl')
@@ -16,13 +29,13 @@ export function prebidMessenger(publisherURL, win = window) {
16
29
  message = JSON.stringify(message);
17
30
  let messagePort;
18
31
  if (onResponse == null) {
19
- win.parent.postMessage(message, prebidDomain);
32
+ target.postMessage(message, prebidDomain);
20
33
  } else {
21
34
  const channel = new MessageChannel();
22
35
  messagePort = channel.port1;
23
36
  messagePort.onmessage = onResponse;
24
37
  win.addEventListener('message', windowListener);
25
- win.parent.postMessage(message, prebidDomain, [channel.port2]);
38
+ target.postMessage(message, prebidDomain, [channel.port2]);
26
39
  }
27
40
 
28
41
  return function stopListening() {
@@ -41,3 +54,11 @@ export function prebidMessenger(publisherURL, win = window) {
41
54
 
42
55
  }
43
56
  }
57
+
58
+ export function renderEventMessage(adId, errorInfo) {
59
+ return Object.assign({
60
+ adId,
61
+ message: PREBID_EVENT,
62
+ event: errorInfo ? AD_RENDER_FAILED : AD_RENDER_SUCCEEDED,
63
+ }, errorInfo ? {info: errorInfo} : null)
64
+ }