prebid-universal-creative 1.17.2 → 1.18.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.
@@ -4,6 +4,16 @@ const karmaConstants = require('karma').constants;
4
4
  const path = require('path');
5
5
 
6
6
  function setBrowsers(karmaConf, browserstack) {
7
+ if (typeof process.getuid === 'function' && process.getuid() === 0) {
8
+ karmaConf.customLaunchers = Object.assign({}, karmaConf.customLaunchers, {
9
+ ChromeHeadlessNoSandbox: {
10
+ base: 'ChromeHeadless',
11
+ flags: ['--no-sandbox']
12
+ }
13
+ });
14
+ karmaConf.browsers = ['ChromeHeadlessNoSandbox'];
15
+ }
16
+
7
17
  if (browserstack) {
8
18
  karmaConf.browserStack = {
9
19
  username: process.env.BROWSERSTACK_USERNAME,
@@ -46,17 +56,19 @@ function setReporters(karmaConf, codeCoverage, browserstack) {
46
56
  function newWebpackConfig(codeCoverage) {
47
57
  const webpackConfig = _.cloneDeep(webpackConf);
48
58
  webpackConfig.devtool = 'inline-source-map';
59
+ delete webpackConfig.optimization;
49
60
 
50
61
  if (codeCoverage) {
51
- webpackConfig.module.rules.push({
52
- test: /\.js$/,
53
- enforce: 'post',
54
- use: {
55
- loader: 'istanbul-instrumenter-loader',
56
- options: { esModules: true }
57
- },
58
- exclude: /(node_modules)|(test)|(resources)|(template)|(testpages)/
62
+ const babelRule = webpackConfig.module.rules.find(rule => {
63
+ const loaders = Array.isArray(rule.use) ? rule.use : [rule.use];
64
+ return loaders.some(loader => loader && loader.loader === 'babel-loader');
59
65
  });
66
+ const babelLoader = babelRule.use.find(loader => loader.loader === 'babel-loader');
67
+ babelLoader.options.plugins = (babelLoader.options.plugins || []).concat([
68
+ ['istanbul', {
69
+ exclude: ['test/**/*.js', 'resources/**/*.js', 'template/**/*.js', 'testpages/**/*.js']
70
+ }]
71
+ ]);
60
72
  }
61
73
  return webpackConfig;
62
74
  }
@@ -88,12 +100,12 @@ module.exports = function(codeCoverage, browserstack, watchMode) {
88
100
  ],
89
101
  webpack: webpackConfig,
90
102
  webpackMiddleware: {
91
- logLevel: 'error'
103
+ stats: 'errors-only'
92
104
  },
93
105
 
94
106
  // frameworks to use
95
107
  // available frameworks: https://npmjs.org/browse/keyword/karma-adapter
96
- frameworks: ['mocha', 'chai', 'sinon'],
108
+ frameworks: ['mocha', 'chai', 'sinon', 'webpack'],
97
109
 
98
110
  // list of files / patterns to load in the browser
99
111
  files: files,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "prebid-universal-creative",
3
- "version": "1.17.2",
3
+ "version": "1.18.0",
4
4
  "description": "Universal creative for Prebid apps",
5
5
  "main": "dist/creative.js",
6
6
  "scripts": {
@@ -28,50 +28,49 @@
28
28
  "@babel/register": "^7.6.2",
29
29
  "@playwright/test": "^1.50.1",
30
30
  "babel-loader": "^8.0.5",
31
+ "babel-plugin-istanbul": "^8.0.0",
31
32
  "babel-plugin-transform-es3-member-expression-literals": "^6.22.0",
32
33
  "babel-plugin-transform-es3-property-literals": "^6.22.0",
33
34
  "chai": "^4.1.2",
34
- "core-js": "^3.13.0",
35
- "core-js-pure": "^3.13.0",
35
+ "core-js": "^3.49.0",
36
+ "core-js-pure": "^3.49.0",
36
37
  "del": "^5.0.0",
37
38
  "execa": "^1.0.0",
38
39
  "gulp": "^5.0.0",
39
40
  "gulp-clean": "^0.4.0",
40
41
  "gulp-connect": "^5.7.0",
41
- "gulp-eslint": "^4.0.2",
42
+ "gulp-eslint": "^6.0.0",
42
43
  "gulp-header": "^2.0.1",
43
44
  "gulp-inject": "^5.0.2",
44
45
  "gulp-rename": "^1.2.2",
45
46
  "gulp-replace": "^0.6.1",
46
47
  "gulp-uglify": "^3.0.0",
47
48
  "istanbul": "^0.4.5",
48
- "istanbul-instrumenter-loader": "^3.0.0",
49
49
  "karma": "^6.4.0",
50
50
  "karma-browserstack-launcher": "^1.3.0",
51
51
  "karma-chai": "^0.1.0",
52
52
  "karma-chrome-launcher": "^2.2.0",
53
- "karma-coverage": "^1.1.2",
54
- "karma-coverage-istanbul-reporter": "^1.4.3",
53
+ "karma-coverage": "^2.2.1",
54
+ "karma-coverage-istanbul-reporter": "^3.0.3",
55
55
  "karma-mocha": "^2.0.1",
56
56
  "karma-mocha-reporter": "^2.2.5",
57
57
  "karma-sinon": "^1.0.5",
58
58
  "karma-sourcemap-loader": "^0.3.7",
59
59
  "karma-spec-reporter": "^0.0.31",
60
- "karma-webpack": "^3.0.5",
60
+ "karma-webpack": "^5.0.1",
61
61
  "lodash": "^4.17.14",
62
- "mocha": "^10.2.0",
62
+ "mocha": "^11.7.6",
63
63
  "opn": "^6.0.0",
64
- "sinon": "^6.3.4",
65
- "string-replace-webpack-plugin": "^0.1.3",
64
+ "sinon": "^22.0.0",
65
+ "string-replace-loader": "^3.3.0",
66
66
  "webdriverio": "^9.10.0",
67
- "webpack": "^3.12.0",
68
- "webpack-common-shake": "^1.0.0",
69
- "webpack-stream": "^4.0.0",
67
+ "webpack": "^5.107.2",
68
+ "webpack-stream": "^7.0.0",
70
69
  "yargs": "^11.0.0"
71
70
  },
72
71
  "dependencies": {
73
72
  "babel-plugin-transform-object-assign": "^6.22.0",
74
- "postscribe": "^2.0.8",
75
- "gulp-cli": "^3.0.0"
73
+ "gulp-cli": "^3.0.0",
74
+ "postscribe": "^2.0.8"
76
75
  }
77
76
  }
package/src/cookieSync.js CHANGED
@@ -24,6 +24,7 @@ const coopSyncParam = parseQueryParam('coop_sync', window.location.search);
24
24
  const COOP_SYNC = !coopSyncParam || coopSyncParam === 'true' || !!parseInt(coopSyncParam);
25
25
  const GDPR = sanitizeGdpr(parseInt(parseQueryParam('gdpr', window.location.search), 10));
26
26
  const GDPR_CONSENT = sanitizeGdprConsent(parseQueryParam('gdpr_consent', window.location.search));
27
+ const ACCOUNT = parseQueryParam('account', window.location.search);
27
28
 
28
29
  /**
29
30
  * checks to make sure URL is valid. Regex from https://validatejs.org/#validators-url, https://gist.github.com/dperini/729294
@@ -267,6 +268,7 @@ function getStringifiedData(endPointArgs) {
267
268
  }
268
269
  };
269
270
  if(BIDDER_ARGS) data.bidders = BIDDER_ARGS;
271
+ if(ACCOUNT) data.account = ACCOUNT;
270
272
 
271
273
  return JSON.stringify(data);
272
274
  }
@@ -24,6 +24,7 @@ const coopSyncParam = parseQueryParam('coop_sync', window.location.search);
24
24
  const COOP_SYNC = !coopSyncParam || coopSyncParam === 'true' || !!parseInt(coopSyncParam);
25
25
  const TIMEOUT = sanitizeTimeout(parseInt(parseQueryParam('timeout', window.location.search), 10));
26
26
  const DEFAULT_GDPR_SCOPE = sanitizeScope(parseInt(parseQueryParam('defaultGdprScope', window.location.search), 10));
27
+ const ACCOUNT = parseQueryParam('account', window.location.search);
27
28
 
28
29
  let consent = {};
29
30
  let syncRan = false;
@@ -284,6 +285,7 @@ function getStringifiedData(endPointArgs) {
284
285
  }
285
286
  };
286
287
  if(BIDDER_ARGS) data.bidders = BIDDER_ARGS;
288
+ if(ACCOUNT) data.account = ACCOUNT;
287
289
 
288
290
  data = attachConsent(data);
289
291
 
@@ -66,7 +66,7 @@ function resizeIframe(width, height) {
66
66
  const iframeHeight = window.innerHeight;
67
67
  if (iframeWidth !== width || iframeHeight !== height) {
68
68
  if (isSafeFrame(window)) {
69
- function resize(status) {
69
+ function resize() {
70
70
  let newWidth = width - iframeWidth;
71
71
  let newHeight = height - iframeHeight;
72
72
  window.$sf.ext.expand({r: newWidth, b: newHeight, push: true});
@@ -131,7 +131,7 @@ function responseCallback(isMobileApp, hbPb) {
131
131
  if (bidObject.burl) {
132
132
  let triggerBurl = function () { triggerPixel(bidObject.burl); };
133
133
  if (isMobileApp) {
134
- let mraidScript = loadScript(window, 'mraid.js',
134
+ loadScript(window, 'mraid.js',
135
135
  function () { // Success loading MRAID
136
136
  let result = registerMRAIDViewableEvent(triggerBurl);
137
137
  if (!result) {
@@ -430,7 +430,7 @@ export function newNativeAssetManager(win, nativeTag, mkMessenger = prebidMessen
430
430
  stopListening();
431
431
  const resize = () => requestHeightResize(
432
432
  bid.adId,
433
- (document.body.clientHeight || document.body.offsetHeight),
433
+ (document.body.clientHeight || document.body.offsetHeight || document.documentElement.scrollHeight),
434
434
  document.body.clientWidth > 1 ? document.body.clientWidth : undefined
435
435
  );
436
436
  document.readyState === 'complete' ? resize() : window.onload = resize;
@@ -1,5 +1,3 @@
1
- import { loadScript, triggerPixel } from "./utils";
2
-
3
1
  const AD_ANCHOR_CLASS_NAME = 'pb-click';
4
2
  const ASSET_ID_ELEMENT_ATTRIBUTE = 'hb_native_asset_id';
5
3
 
@@ -1,7 +1,7 @@
1
1
  /*
2
2
  * Script to handle firing impression and click trackers from native teamplates
3
3
  */
4
- import { parseUrl, triggerPixel, transformAuctionTargetingData } from './utils';
4
+ import { triggerPixel, transformAuctionTargetingData } from './utils';
5
5
  import { newNativeAssetManager } from './nativeAssetManager';
6
6
  import {prebidMessenger} from './messaging.js';
7
7
 
@@ -3,8 +3,69 @@ import postscribe from 'postscribe';
3
3
  export function writeAdHtml(markup, ps = postscribe) {
4
4
  // remove <?xml> and <!doctype> tags
5
5
  // https://github.com/prebid/prebid-universal-creative/issues/134
6
- markup = markup.replace(/\<(\?xml|(\!DOCTYPE[^\>\[]+(\[[^\]]+)?))+[^>]+\>/gi, '');
7
- ps(document.body, markup, {
6
+ markup = markup
7
+ .replace(/<\?xml\b[^>]*(?:\?>|>)/gi, '')
8
+ .replace(/<!doctype\b[^>\[]*(?:\[[^\]]*\][^>]*)?>/gi, '');
9
+
10
+ let finalMarkup;
11
+
12
+ try {
13
+ finalMarkup = normalizeMarkup(markup);
14
+ } catch (error) {
15
+ console.error("Error normalizing markup:", error.message);
16
+ finalMarkup = markup;
17
+ }
18
+
19
+ ps(document.body, finalMarkup, {
8
20
  error: console.error
9
21
  });
10
22
  }
23
+
24
+ /**
25
+ * Normalizes an HTML string by parsing and re-serializing it,
26
+ * returning the content between custom PUC_START and PUC_END markers.
27
+ *
28
+ * This function is specifically aimed at addressing an issue with `postscribe` where double quotes inside single-quoted
29
+ * HTML attributes are not correctly escaped.
30
+ */
31
+ function normalizeMarkup(markup) {
32
+ const timestamp = Date.now();
33
+ const startMarkerId = `PUC_START_${timestamp}`;
34
+ const endMarkerId = `PUC_END_${timestamp}`;
35
+ const startMarker = `<div id="${startMarkerId}"></div>`;
36
+ const endMarker = `<div id="${endMarkerId}"></div>`;
37
+ const doc = new DOMParser().parseFromString(`${startMarker}${markup}${endMarker}`, "text/html");
38
+
39
+ const textMap = new Map();
40
+ let textId = 0;
41
+
42
+ const replaceTextNodes = (node) => {
43
+ if (node.nodeType === Node.TEXT_NODE && node.textContent.trim()) {
44
+ const id = `PUC_NODE_TEXT_${textId++}_${timestamp}`;
45
+ textMap.set(id, node.textContent);
46
+ const span = doc.createElement("span");
47
+ span.dataset.textId = id;
48
+ node.parentNode.replaceChild(span, node);
49
+ } else {
50
+ [...node.childNodes].forEach(replaceTextNodes);
51
+ }
52
+ };
53
+
54
+ let current = doc.querySelector(`#${startMarkerId}`).nextSibling;
55
+ const end = doc.querySelector(`#${endMarkerId}`);
56
+ while (current && current !== end) {
57
+ replaceTextNodes(current);
58
+ current = current.nextSibling;
59
+ }
60
+
61
+ const serialized = new XMLSerializer().serializeToString(doc);
62
+ const snippet = serialized
63
+ .split(startMarker)[1]
64
+ .split(endMarker)[0]
65
+ .replace(
66
+ /<span data-text-id="(PUC_NODE_TEXT_\d+_\d+)"[^>]*><\/span>/g,
67
+ (_, id) => textMap.get(id) || ""
68
+ );
69
+
70
+ return snippet.trim();
71
+ }
@@ -1,4 +1,4 @@
1
- import { parseUrl, transformAuctionTargetingData } from './utils';
1
+ import { transformAuctionTargetingData } from './utils';
2
2
  import { canLocatePrebid } from './environment';
3
3
  import { insertElement, getEmptyIframe } from './domHelper';
4
4
  import {prebidMessenger, renderEventMessage} from './messaging.js';
@@ -2,91 +2,28 @@ import { renderAmpOrMobileAd } from 'src/mobileAndAmpRender';
2
2
  import * as postscribeRender from 'src/postscribeRender'
3
3
  import * as utils from 'src/utils';
4
4
  import { expect } from 'chai';
5
- import { mocks } from 'test/helpers/mocks';
6
- import { merge } from 'lodash';
7
- import {writeAdHtml} from 'src/postscribeRender';
8
-
9
-
10
- function renderingMocks() {
11
- return {
12
- messages: [],
13
- getWindowObject: function () {
14
- return {
15
- document: {
16
- body: {
17
- appendChild: sinon.spy(),
18
- },
19
- createComment: () => true,
20
- },
21
- parent: {
22
- postMessage: sinon.spy(),
23
- $$PREBID_GLOBAL$$: {
24
- renderAd: sinon.spy(),
25
- },
26
- },
27
- postMessage: (message, domain) => {
28
- this.messages[0](message);
29
- },
30
- top: null,
31
- $sf: {
32
- ext: {
33
- register: sinon.spy(),
34
- expand: sinon.spy(),
35
- },
36
- },
37
- addEventListener: (type, listener, capture) => {
38
- this.messages.push(listener);
39
- },
40
- innerWidth: 300,
41
- innerHeight: 250,
42
- };
43
- },
44
- };
45
- }
46
-
47
- describe("renderingManager", function () {
48
- let xhr;
49
- let requests;
50
-
51
- before(function () {
52
- xhr = sinon.useFakeXMLHttpRequest();
53
- xhr.onCreate = (request) => requests.push(request);
54
- });
55
-
56
- beforeEach(function () {
57
- requests = [];
58
- });
5
+ import { writeAdHtml } from 'src/postscribeRender';
59
6
 
60
- after(function () {
61
- xhr.restore();
62
- });
63
7
 
8
+ describe("renderingManager", function () {
64
9
  describe("mobile creative", function () {
10
+ let sandbox;
65
11
  let writeHtmlSpy;
66
- let sendRequestSpy;
67
- let triggerPixelSpy;
68
- let mockWin;
69
-
70
- before(function () {
71
- writeHtmlSpy = sinon.spy(postscribeRender, "writeAdHtml");
72
- sendRequestSpy = sinon.spy(utils, "sendRequest");
73
- triggerPixelSpy = sinon.spy(utils, "triggerPixel");
74
- mockWin = merge(
75
- mocks.createFakeWindow("http://example.com"),
76
- renderingMocks().getWindowObject()
77
- );
78
- });
12
+ let sendRequestStub;
13
+ let requestCallbacks;
79
14
 
80
- afterEach(function () {
81
- writeHtmlSpy.resetHistory();
82
- sendRequestSpy.resetHistory();
83
- triggerPixelSpy.resetHistory();
15
+ beforeEach(function () {
16
+ sandbox = sinon.createSandbox();
17
+ requestCallbacks = [];
18
+ writeHtmlSpy = sandbox.spy(postscribeRender, "writeAdHtml");
19
+ sendRequestStub = sandbox.stub(utils, "sendRequest").callsFake((url, callback) => {
20
+ requestCallbacks.push(callback);
21
+ });
22
+ sandbox.spy(utils, "triggerPixel");
84
23
  });
85
24
 
86
- after(function () {
87
- writeHtmlSpy.restore();
88
- sendRequestSpy.restore();
89
- triggerPixelSpy.restore();
25
+ afterEach(function () {
26
+ sandbox.restore();
90
27
  });
91
28
 
92
29
  it("should render mobile app creative", function () {
@@ -106,9 +43,9 @@ describe("renderingManager", function () {
106
43
  adm: "ad-markup",
107
44
  wurl: "https://test.prebidcache.wurl",
108
45
  };
109
- requests[0].respond(200, {}, JSON.stringify(response));
46
+ requestCallbacks[0](JSON.stringify(response));
110
47
  expect(writeHtmlSpy.callCount).to.equal(1);
111
- expect(sendRequestSpy.args[0][0]).to.equal(
48
+ expect(sendRequestStub.args[0][0]).to.equal(
112
49
  "https://example.com/path?uuid=123"
113
50
  );
114
51
  });
@@ -129,9 +66,9 @@ describe("renderingManager", function () {
129
66
  crid: 123,
130
67
  adm: "ad-markup",
131
68
  };
132
- requests[0].respond(200, {}, JSON.stringify(response));
69
+ requestCallbacks[0](JSON.stringify(response));
133
70
  expect(writeHtmlSpy.callCount).to.equal(1);
134
- expect(sendRequestSpy.args[0][0]).to.equal(
71
+ expect(sendRequestStub.args[0][0]).to.equal(
135
72
  "https://example.com/path?uuid=123"
136
73
  );
137
74
  });
@@ -149,9 +86,9 @@ describe("renderingManager", function () {
149
86
  crid: 123,
150
87
  adm: "ad-markup",
151
88
  };
152
- requests[0].respond(200, {}, JSON.stringify(response));
89
+ requestCallbacks[0](JSON.stringify(response));
153
90
  expect(writeHtmlSpy.callCount).to.equal(1);
154
- expect(sendRequestSpy.args[0][0]).to.equal(
91
+ expect(sendRequestStub.args[0][0]).to.equal(
155
92
  "https://prebid.adnxs.com/pbc/v1/cache?uuid=123"
156
93
  );
157
94
  });
@@ -178,7 +115,7 @@ describe("renderingManager", function () {
178
115
  // crid: 123,
179
116
  // adm: '<script src="notExistingScript.js"></script>'
180
117
  // };
181
- // requests[0].respond(200, {}, JSON.stringify(response));
118
+ // requestCallbacks[0](JSON.stringify(response));
182
119
 
183
120
  // setTimeout(() => {
184
121
  // expect(consoleErrorSpy.callCount).to.equal(1);
@@ -192,14 +129,17 @@ describe("renderingManager", function () {
192
129
  let writeHtmlSpy;
193
130
  let sendRequestSpy;
194
131
  let triggerPixelSpy;
195
- let mockWin;
196
132
  let ucTagData;
197
133
  let response;
134
+ let requestCallbacks;
198
135
 
199
136
  beforeEach(function () {
200
- sandbox = sinon.sandbox.create();
137
+ sandbox = sinon.createSandbox();
138
+ requestCallbacks = [];
201
139
  writeHtmlSpy = sandbox.spy(postscribeRender, "writeAdHtml");
202
- sendRequestSpy = sandbox.spy(utils, "sendRequest");
140
+ sendRequestSpy = sandbox.stub(utils, "sendRequest").callsFake((url, callback) => {
141
+ requestCallbacks.push(callback);
142
+ });
203
143
  triggerPixelSpy = sandbox.spy(utils, "triggerPixel");
204
144
  ucTagData = {
205
145
  cacheHost: "example.com",
@@ -226,21 +166,21 @@ describe("renderingManager", function () {
226
166
  sandbox.spy(window.parent, 'postMessage');
227
167
  ucTagData.size = '400x500'
228
168
  renderAmpOrMobileAd(ucTagData);
229
- requests[0].respond(200, {}, JSON.stringify(response));
169
+ requestCallbacks[0](JSON.stringify(response));
230
170
  sinon.assert.calledWith(window.parent.postMessage, {
231
171
  sentinel: "amp",
232
172
  type: "embed-size",
233
173
  width: 400,
234
174
  height: 500,
235
175
  });
236
- })
176
+ });
237
177
 
238
178
  it("should render amp creative", function () {
239
179
  ucTagData.hbPb = "10.00";
240
180
  renderAmpOrMobileAd(ucTagData);
241
181
 
242
182
 
243
- requests[0].respond(200, {}, JSON.stringify(response));
183
+ requestCallbacks[0](JSON.stringify(response));
244
184
  expect(writeHtmlSpy.args[0][0]).to.equal(
245
185
  "<!--Creative 123 served by Prebid.js Header Bidding-->ad-markup10.00"
246
186
  );
@@ -255,7 +195,7 @@ describe("renderingManager", function () {
255
195
  it("should replace AUCTION_PRICE with response.price over hbPb", function () {
256
196
  renderAmpOrMobileAd(ucTagData);
257
197
  response.price = 12.5;
258
- requests[0].respond(200, {}, JSON.stringify(response));
198
+ requestCallbacks[0](JSON.stringify(response));
259
199
  expect(writeHtmlSpy.args[0][0]).to.equal(
260
200
  "<!--Creative 123 served by Prebid.js Header Bidding-->ad-markup12.5"
261
201
  );
@@ -269,7 +209,7 @@ describe("renderingManager", function () {
269
209
 
270
210
  it("should replace AUCTION_PRICE with with empty value when neither price nor hbPb exist", function () {
271
211
  renderAmpOrMobileAd(ucTagData);
272
- requests[0].respond(200, {}, JSON.stringify(response));
212
+ requestCallbacks[0](JSON.stringify(response));
273
213
  expect(writeHtmlSpy.args[0][0]).to.equal(
274
214
  "<!--Creative 123 served by Prebid.js Header Bidding-->ad-markup"
275
215
  );
@@ -301,9 +241,62 @@ describe('writeAdHtml', () => {
301
241
  sinon.assert.calledWith(ps, sinon.match.any, '<div>mock-ad</div>')
302
242
  });
303
243
 
244
+ it('removes XML declarations from markup', () => {
245
+ const ps = sinon.stub();
246
+ writeAdHtml('<?xml version="1.0" encoding="UTF-8"?><div>mock-ad</div>', ps);
247
+ sinon.assert.calledWith(ps, sinon.match.any, '<div>mock-ad</div>')
248
+ });
249
+
250
+ it('removes malformed XML declarations from markup', () => {
251
+ const ps = sinon.stub();
252
+ writeAdHtml('<?xml version="1.0"><div>mock-ad</div>', ps);
253
+ sinon.assert.calledWith(ps, sinon.match.any, '<div>mock-ad</div>')
254
+ });
255
+
256
+ it('removes doctype declarations with internal subsets from markup for postscribe compatibility (see postscribe issue 358)', () => {
257
+ const ps = sinon.stub();
258
+ writeAdHtml('<!DOCTYPE html [<!ENTITY nbsp "&#160;">]><div>mock-ad</div>', ps);
259
+ sinon.assert.calledWith(ps, sinon.match.any, '<div>mock-ad</div>')
260
+ });
261
+
304
262
  it('should execute script tag inserted into the body', () => {
305
263
  const markup = '<script>window.testScriptExecuted=true;</script>'
306
264
  writeAdHtml(markup);
307
265
  expect(window.testScriptExecuted).to.equal(true);
308
266
  });
267
+
268
+ it('should handle single quotes with inner double quotes', () => {
269
+ const input = `<img title='uh "oh" > this should all be inside the title attribute'>`;
270
+ console.log('Input: ', input);
271
+
272
+ writeAdHtml(input);
273
+
274
+ const img = document.querySelector('img:last-of-type');
275
+ if (img) {
276
+ console.log('Output:', img.outerHTML);
277
+ console.log('Title: ', img.getAttribute('title'));
278
+
279
+ const expected = 'uh "oh" > this should all be inside the title attribute';
280
+ expect(img.getAttribute('title')).to.equal(expected);
281
+ }
282
+ });
283
+
284
+ it('should handle JSON in data attributes with single quotes', () => {
285
+ const input = `<div data-json='{"key": "value"}'>Test</div>`;
286
+ console.log('Input: ', input);
287
+
288
+ writeAdHtml(input);
289
+
290
+ const div = document.querySelector('div[data-json]:last-of-type');
291
+ if (div) {
292
+ console.log('Output:', div.outerHTML);
293
+ console.log('Data: ', div.getAttribute('data-json'));
294
+
295
+ const dataJson = div.getAttribute('data-json');
296
+ expect(dataJson).to.equal('{"key": "value"}');
297
+
298
+ const parsed = JSON.parse(dataJson);
299
+ expect(parsed.key).to.equal('value');
300
+ }
301
+ });
309
302
  })
@@ -43,7 +43,7 @@ const mockDocument = {
43
43
  };
44
44
 
45
45
  // creates mock postmessage response from prebid's native.js:getAssetMessage
46
- function createResponder(assets,url,template, clickUrlUnesc = '') {
46
+ function createResponder(assets,url,template) {
47
47
  return function(type, listener) {
48
48
  if (type !== 'message') { return; }
49
49
 
@@ -443,7 +443,7 @@ describe('nativeAssetManager', () => {
443
443
  expect(win.document.body.innerHTML).to.not.include(`##hb_native_asset_id_4##`);
444
444
 
445
445
  // test that we are replacing ALL asset occurrences
446
- expect(([...win.document.body.innerHTML.match(/new value/g)] || []).length, "expected 2 occurrences of \"new value\"").to.equal(2);
446
+ expect((win.document.body.innerHTML.match(/new value/g) || []).length, "expected 2 occurrences of \"new value\"").to.equal(2);
447
447
  });
448
448
 
449
449
  it('no placeholders found but requests all assets flag set - adTemplate', () => {
@@ -522,7 +522,7 @@ describe('nativeAssetManager', () => {
522
522
  let cb = sinon.spy();
523
523
  let targetingData = {
524
524
  uuid: '123'
525
- }
525
+ };
526
526
 
527
527
  sinon.stub(utils, 'sendRequest').callsFake(function(arg1, cb) {
528
528
  let response = JSON.stringify({
@@ -560,7 +560,7 @@ describe('nativeAssetManager', () => {
560
560
  // cta was not in the response so it should default to an empty string
561
561
  expect(win.document.body.innerHTML).to.include('<h1></h1>');
562
562
  expect(cb.getCall(0).args[0]).to.haveOwnProperty('eventtrackers');
563
- })
563
+ });
564
564
  })
565
565
 
566
566
  describe('safe frame disabled', () => {
@@ -620,14 +620,26 @@ describe('nativeAssetManager', () => {
620
620
  expect(win.document.body.innerHTML).to.include(`<a href="http://www.example.com" target="_blank" class="pb-click">new value</a>`);
621
621
  });
622
622
 
623
+ function getResizeRequest() {
624
+ return win.parent.postMessage.args
625
+ .map(([msg]) => JSON.parse(msg))
626
+ .find((msg) => msg.action === 'resizeNativeHeight')
627
+ }
628
+
623
629
  it('should not request width resize if width is 1', () => {
624
630
  sandbox.stub(document.body, 'clientWidth').get(() => 1);
625
631
  const nativeAssetManager = makeManager();
626
632
  nativeAssetManager.loadAssets(AD_ID);
627
- const resizeRequest = win.parent.postMessage.args
628
- .map(([msg]) => JSON.parse(msg))
629
- .find((msg) => msg.action === 'resizeNativeHeight')
630
- expect(resizeRequest.width).to.not.exist;
633
+ expect(getResizeRequest().width).to.not.exist;
634
+ });
635
+
636
+ it('should use scrollHeight if offsetHeight & clientHeight are 0', () => {
637
+ sandbox.stub(document.body, 'clientHeight').get(() => 0);
638
+ sandbox.stub(document.body, 'offsetHeight').get(() => 0);
639
+ sandbox.stub(document.documentElement, 'scrollHeight').get(() => 123);
640
+ const nativeAssetManager = makeManager();
641
+ nativeAssetManager.loadAssets(AD_ID);
642
+ expect(getResizeRequest().height).to.eql(123);
631
643
  })
632
644
 
633
645
  it('should set the iframe to the width of the container', () => {
@@ -14,11 +14,11 @@ describe('test firing native trackers', function () {
14
14
 
15
15
  getElementsByClassName = sinon.stub(document, 'getElementsByClassName').callsFake(() => {
16
16
  return [{
17
- addEventListener: (event, callback, capture) => {
17
+ addEventListener: (event, callback) => {
18
18
  // immediately call the callback to test the click
19
19
  callback({
20
20
  target: {
21
- getAttribute: (name) => {
21
+ getAttribute: () => {
22
22
  return 1;
23
23
  }
24
24
  }