From d4facd737906351bd743b5acce65225f72933894 Mon Sep 17 00:00:00 2001 From: Demetrio Girardi Date: Thu, 11 Aug 2022 10:39:33 -0700 Subject: [PATCH 1/3] Prebid core: add utility to retrieve user agent client hints --- libraries/fpd/sua.js | 95 ++++++++++++++++ test/spec/fpd/sua_spec.js | 233 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 328 insertions(+) create mode 100644 libraries/fpd/sua.js create mode 100644 test/spec/fpd/sua_spec.js diff --git a/libraries/fpd/sua.js b/libraries/fpd/sua.js new file mode 100644 index 00000000000..1f4d9dfd646 --- /dev/null +++ b/libraries/fpd/sua.js @@ -0,0 +1,95 @@ +import {isEmptyStr, isStr} from '../../src/utils.js'; + +export const SUA_SOURCE_UNKNOWN = 0; +export const SUA_SOURCE_LOW_ENTROPY = 1; +export const SUA_SOURCE_HIGH_ENTROPY = 2; +export const SUA_SOURCE_UA_HEADER = 3; + +// "high entropy" (i.e. privacy-sensitive) fields that can be requested from the navigator. +export const HIGH_ENTROPY_HINTS = [ + 'architecture', + 'bitness', + 'model', + 'platformVersion', + 'fullVersionList' +] + +/** + * Returns low entropy UA client hints encoded as an ortb2.6 device.sua object; or null if no UA client hints are available. + */ +export const getLowEntropySUA = lowEntropySUAAccessor(); + +/** + * Returns a promise to high entropy UA client hints encoded as an ortb2.6 device.sua object, or null if no UA client hints are available. + * + * Note that the return value is a promise because the underlying browser API returns a promise; this + * seems to plan for additional controls (such as alerts / permission request prompts to the user); it's unclear + * at the moment if this means that asking for more hints would result in slower / more expensive calls. + * + * @param {Array[String]} hints hints to request, defaults to all (HIGH_ENTROPY_HINTS). + */ +export const getHighEntropySUA = highEntropySUAAccessor(); + +export function lowEntropySUAAccessor(uaData = window.navigator?.userAgentData) { + const sua = uaData == null ? null : Object.freeze(uaDataToSUA(SUA_SOURCE_LOW_ENTROPY, uaData)); + return function () { + return sua; + } +} + +export function highEntropySUAAccessor(uaData = window.navigator?.userAgentData) { + const cache = {}; + const keys = new WeakMap(); + return function (hints = HIGH_ENTROPY_HINTS) { + if (!keys.has(hints)) { + const sorted = Array.from(hints); + sorted.sort(); + keys.set(hints, sorted.join('|')); + } + const key = keys.get(hints); + if (!cache.hasOwnProperty(key)) { + try { + cache[key] = uaData.getHighEntropyValues(hints).then(result => Object.freeze(uaDataToSUA(SUA_SOURCE_HIGH_ENTROPY, result))).catch(() => null); + } catch (e) { + cache[key] = Promise.resolve(null); + } + } + return cache[key]; + } +} + +/** + * Convert a User Agent client hints object to an ORTB 2.6 device.sua fragment + * https://iabtechlab.com/wp-content/uploads/2022/04/OpenRTB-2-6_FINAL.pdf + * + * @param source source of the UAData object (0 to 3) + * @param uaData https://developer.mozilla.org/en-US/docs/Web/API/NavigatorUAData/ + * @return {{}} + */ +export function uaDataToSUA(source, uaData) { + function toBrandVersion(brand, version) { + const bv = {brand}; + if (isStr(version) && !isEmptyStr(version)) { + bv.version = version.split('.'); + } + return bv; + } + + const sua = {source}; + if (uaData.platform) { + sua.platform = toBrandVersion(uaData.platform, uaData.platformVersion); + } + if (uaData.fullVersionList || uaData.brands) { + sua.browsers = (uaData.fullVersionList || uaData.brands).map(({brand, version}) => toBrandVersion(brand, version)); + } + if (uaData.hasOwnProperty('mobile')) { + sua.mobile = uaData.mobile ? 1 : 0; + } + ['model', 'bitness', 'architecture'].forEach(prop => { + const value = uaData[prop]; + if (isStr(value)) { + sua[prop] = value; + } + }) + return sua; +} diff --git a/test/spec/fpd/sua_spec.js b/test/spec/fpd/sua_spec.js new file mode 100644 index 00000000000..b59f53a427f --- /dev/null +++ b/test/spec/fpd/sua_spec.js @@ -0,0 +1,233 @@ +import { + highEntropySUAAccessor, + lowEntropySUAAccessor, + SUA_SOURCE_HIGH_ENTROPY, + SUA_SOURCE_LOW_ENTROPY, + SUA_SOURCE_UNKNOWN, + suaFromUAData, + uaDataToSUA +} from '../../../libraries/fpd/sua.js'; + +describe('uaDataToSUA', () => { + Object.entries({ + 'platform': 'platform', + 'browsers': 'brands', + 'mobile': 'mobile', + 'architecture': 'architecture', + 'model': 'model', + 'bitness': 'bitness' + }).forEach(([suaKey, uaKey]) => { + it(`should not set ${suaKey} if ${uaKey} is missing from UAData`, () => { + const example = { + platform: 'Windows', + brands: [{brand: 'Mock', version: 'mk'}], + mobile: true, + model: 'mockModel', + bitness: '64', + architecture: 'arm' + } + delete example[uaKey]; + const sua = uaDataToSUA(SUA_SOURCE_UNKNOWN, example); + expect(sua.hasOwnProperty(suaKey)).to.be.false; + }) + }) + it('should convert low-entropy userAgentData', () => { + const sua = uaDataToSUA(SUA_SOURCE_LOW_ENTROPY, { + 'brands': [ + { + 'brand': '.Not/A)Brand', + 'version': '99' + }, + { + 'brand': 'Google Chrome', + 'version': '103' + }, + { + 'brand': 'Chromium', + 'version': '103' + } + ], + 'mobile': false, + 'platform': 'Linux' + }); + + expect(sua).to.eql({ + source: SUA_SOURCE_LOW_ENTROPY, + mobile: 0, + platform: { + brand: 'Linux', + }, + browsers: [ + { + brand: '.Not/A)Brand', + version: [ + '99' + ] + }, + { + brand: 'Google Chrome', + version: [ + '103' + ] + }, + { + brand: 'Chromium', + version: [ + '103' + ] + } + ] + }) + }); + + it('should convert high entropy properties', () => { + const uaData = { + architecture: 'x86', + bitness: '64', + fullVersionList: [ + { + 'brand': '.Not/A)Brand', + 'version': '99.0.0.0' + }, + { + 'brand': 'Google Chrome', + 'version': '103.0.5060.134' + }, + { + 'brand': 'Chromium', + 'version': '103.0.5060.134' + } + ], + brands: [ + { + 'brand': '.Not/A)Brand', + 'version': '99' + }, + { + 'brand': 'Google Chrome', + 'version': '103' + }, + { + 'brand': 'Chromium', + 'version': '103' + } + ], + model: 'mockModel', + platform: 'Linux', + platformVersion: '5.14.0' + } + + expect(uaDataToSUA(SUA_SOURCE_HIGH_ENTROPY, uaData)).to.eql({ + source: SUA_SOURCE_HIGH_ENTROPY, + architecture: 'x86', + bitness: '64', + model: 'mockModel', + platform: { + brand: 'Linux', + version: [ + '5', + '14', + '0' + ] + }, + browsers: [ + { + brand: '.Not/A)Brand', + version: [ + '99', + '0', + '0', + '0' + ] + }, + { + brand: 'Google Chrome', + version: [ + '103', + '0', + '5060', + '134' + ] + }, + { + brand: 'Chromium', + version: [ + '103', + '0', + '5060', + '134' + ] + } + ] + }) + }) +}); + +describe('lowEntropySUAAccessor', () => { + function getSUA(uaData) { + return lowEntropySUAAccessor(uaData)(); + } + + it('should not be modifiable', () => { + const sua = getSUA({}); + expect(() => { sua.prop = 'value'; }).to.throw(); + }); + + it('should return null if no uaData is available', () => { + expect(getSUA(null)).to.eql(null); + }) +}); + +describe('highEntropySUAAccessor', () => { + let userAgentData, uaResult, getSUA; + beforeEach(() => { + uaResult = {}; + userAgentData = { + getHighEntropyValues: sinon.stub().callsFake(() => Promise.resolve(uaResult)) + }; + getSUA = highEntropySUAAccessor(userAgentData); + }); + + describe('should resolve to null if', () => { + it('uaData is not available', () => { + getSUA = highEntropySUAAccessor(null); + return getSUA().then((result) => { + expect(result).to.eql(null); + }) + }); + it('getHighEntropyValues is not avialable', () => { + delete userAgentData.getHighEntropyValues; + return getSUA().then((result) => { + expect(result).to.eql(null); + }) + }); + it('getHighEntropyValues throws', () => { + userAgentData.getHighEntropyValues.callsFake(() => { throw new Error() }); + return getSUA().then((result) => { + expect(result).to.eql(null); + }) + }); + it('getHighEntropyValues rejects', () => { + userAgentData.getHighEntropyValues.callsFake(() => Promise.reject(new Error())); + return getSUA().then((result) => { + expect(result).to.eql(null); + }) + }); + }); + it('should pass hints to userAgentData', () => { + getSUA(['h1', 'h2']); + sinon.assert.calledWith(userAgentData.getHighEntropyValues, ['h1', 'h2']); + }); + + it('should cache results for a set of hints', () => { + getSUA(['h1', 'h2']); + getSUA(['h2', 'h1']); + sinon.assert.calledOnce(userAgentData.getHighEntropyValues); + }); + + it('should return unmodifiable objects', () => { + return getSUA().then(result => { + expect(() => { result.prop = 'value'; }).to.throw(); + }) + }) +}) From 0f5f4870259af93a81065ca476fce953ce0ff84f Mon Sep 17 00:00:00 2001 From: Demetrio Girardi Date: Tue, 16 Aug 2022 10:12:01 -0700 Subject: [PATCH 2/3] Cache enrichments FPD between auctions --- modules/enrichmentFpdModule.js | 24 ++++++++++--------- test/spec/modules/enrichmentFpdModule_spec.js | 12 +++++++++- test/spec/modules/fpdModule_spec.js | 2 ++ 3 files changed, 26 insertions(+), 12 deletions(-) diff --git a/modules/enrichmentFpdModule.js b/modules/enrichmentFpdModule.js index 139b03d6189..4585e2f4ce1 100644 --- a/modules/enrichmentFpdModule.js +++ b/modules/enrichmentFpdModule.js @@ -8,7 +8,7 @@ import { submodule } from '../src/hook.js'; import {getRefererInfo, parseDomain} from '../src/refererDetection.js'; import { getCoreStorageManager } from '../src/storageManager.js'; -let ortb2 = {}; +let ortb2; let win = (window === window.top) ? window : window.top; export const coreStorage = getCoreStorageManager('enrichmentFpd'); @@ -127,7 +127,7 @@ function setKeywords() { /** * Resets modules global ortb2 data */ -const resetOrtb2 = () => { ortb2 = {} }; +export const resetEnrichments = () => { ortb2 = null }; function runEnrichments() { setReferer(); @@ -139,17 +139,19 @@ function runEnrichments() { return ortb2; } -/** - * Sets default values to ortb2 if exists and adds currency and ortb2 setConfig callbacks on init - */ export function processFpd(fpdConf, {global}) { - resetOrtb2(); - - return { - global: (!fpdConf.skipEnrichments) ? mergeDeep(runEnrichments(), global) : global - }; + if (fpdConf.skipEnrichments) { + return {global}; + } else { + if (ortb2 == null) { + ortb2 = {}; + runEnrichments(); + } + return { + global: mergeDeep({}, ortb2, global) + }; + } } - /** @type {firstPartyDataSubmodule} */ export const enrichmentsSubmodule = { name: 'enrichments', diff --git a/test/spec/modules/enrichmentFpdModule_spec.js b/test/spec/modules/enrichmentFpdModule_spec.js index 7d7e463c015..4f5be56e6d4 100644 --- a/test/spec/modules/enrichmentFpdModule_spec.js +++ b/test/spec/modules/enrichmentFpdModule_spec.js @@ -1,6 +1,6 @@ import { expect } from 'chai'; import { getRefererInfo } from 'src/refererDetection.js'; -import { processFpd, coreStorage } from 'modules/enrichmentFpdModule.js'; +import { processFpd, coreStorage, resetEnrichments } from 'modules/enrichmentFpdModule.js'; describe('the first party data enrichment module', function() { let width; @@ -20,6 +20,7 @@ describe('the first party data enrichment module', function() { }); beforeEach(function() { + resetEnrichments(); querySelectorStub = sinon.stub(window.top.document, 'querySelector'); querySelectorStub.withArgs("link[rel='canonical']").returns(canonical); querySelectorStub.withArgs("meta[name='keywords']").returns(keywords); @@ -98,4 +99,13 @@ describe('the first party data enrichment module', function() { expect(validated.device).to.deep.equal({ w: 1200, h: 700 }); expect(validated.site.keywords).to.be.undefined; }); + + it('does not run enrichments again on the second call', () => { + width = 1; + height = 2; + const first = processFpd({}, {}).global; + width = 3; + const second = processFpd({}, {}).global; + expect(first).to.eql(second); + }) }); diff --git a/test/spec/modules/fpdModule_spec.js b/test/spec/modules/fpdModule_spec.js index 498bed29243..6c138db30d3 100644 --- a/test/spec/modules/fpdModule_spec.js +++ b/test/spec/modules/fpdModule_spec.js @@ -4,6 +4,7 @@ import {getRefererInfo} from 'src/refererDetection.js'; import {processFpd, registerSubmodules, startAuctionHook, reset} from 'modules/fpdModule/index.js'; import * as enrichmentModule from 'modules/enrichmentFpdModule.js'; import * as validationModule from 'modules/validationFpdModule/index.js'; +import {resetEnrichments} from 'modules/enrichmentFpdModule.js'; describe('the first party data module', function () { afterEach(function () { @@ -70,6 +71,7 @@ describe('the first party data module', function () { }); beforeEach(function() { + resetEnrichments(); querySelectorStub = sinon.stub(window.top.document, 'querySelector'); querySelectorStub.withArgs("link[rel='canonical']").returns(canonical); querySelectorStub.withArgs("meta[name='keywords']").returns(keywords); From 6fa4d4edadf209238b82e2f7e1798ecf4693cf8b Mon Sep 17 00:00:00 2001 From: Demetrio Girardi Date: Tue, 16 Aug 2022 11:08:37 -0700 Subject: [PATCH 3/3] Set device.sua from fpdEnrichment --- libraries/fpd/sua.js | 11 +++-- modules/enrichmentFpdModule.js | 29 +++++++++--- test/spec/fpd/sua_spec.js | 13 +++++- test/spec/modules/enrichmentFpdModule_spec.js | 45 ++++++++++++++++--- test/spec/modules/fpdModule_spec.js | 7 +++ 5 files changed, 87 insertions(+), 18 deletions(-) diff --git a/libraries/fpd/sua.js b/libraries/fpd/sua.js index 1f4d9dfd646..b6a46763514 100644 --- a/libraries/fpd/sua.js +++ b/libraries/fpd/sua.js @@ -1,4 +1,5 @@ -import {isEmptyStr, isStr} from '../../src/utils.js'; +import {isEmptyStr, isStr, isEmpty} from '../../src/utils.js'; +import {GreedyPromise} from '../../src/utils/promise.js'; export const SUA_SOURCE_UNKNOWN = 0; export const SUA_SOURCE_LOW_ENTROPY = 1; @@ -31,7 +32,7 @@ export const getLowEntropySUA = lowEntropySUAAccessor(); export const getHighEntropySUA = highEntropySUAAccessor(); export function lowEntropySUAAccessor(uaData = window.navigator?.userAgentData) { - const sua = uaData == null ? null : Object.freeze(uaDataToSUA(SUA_SOURCE_LOW_ENTROPY, uaData)); + const sua = isEmpty(uaData) ? null : Object.freeze(uaDataToSUA(SUA_SOURCE_LOW_ENTROPY, uaData)); return function () { return sua; } @@ -49,9 +50,11 @@ export function highEntropySUAAccessor(uaData = window.navigator?.userAgentData) const key = keys.get(hints); if (!cache.hasOwnProperty(key)) { try { - cache[key] = uaData.getHighEntropyValues(hints).then(result => Object.freeze(uaDataToSUA(SUA_SOURCE_HIGH_ENTROPY, result))).catch(() => null); + cache[key] = uaData.getHighEntropyValues(hints).then(result => { + return isEmpty(result) ? null : Object.freeze(uaDataToSUA(SUA_SOURCE_HIGH_ENTROPY, result)) + }).catch(() => null); } catch (e) { - cache[key] = Promise.resolve(null); + cache[key] = GreedyPromise.resolve(null); } } return cache[key]; diff --git a/modules/enrichmentFpdModule.js b/modules/enrichmentFpdModule.js index 4585e2f4ce1..3e1fa496584 100644 --- a/modules/enrichmentFpdModule.js +++ b/modules/enrichmentFpdModule.js @@ -7,11 +7,15 @@ import { timestamp, mergeDeep } from '../src/utils.js'; import { submodule } from '../src/hook.js'; import {getRefererInfo, parseDomain} from '../src/refererDetection.js'; import { getCoreStorageManager } from '../src/storageManager.js'; +import {GreedyPromise} from '../src/utils/promise.js'; +import {getHighEntropySUA, getLowEntropySUA} from '../libraries/fpd/sua.js'; let ortb2; let win = (window === window.top) ? window : window.top; export const coreStorage = getCoreStorageManager('enrichmentFpd'); +export const sua = {he: getHighEntropySUA, le: getLowEntropySUA}; + /** * Find the root domain * @param {string|undefined} fullDomain @@ -124,32 +128,45 @@ function setKeywords() { if (keywords && keywords.content) mergeDeep(ortb2, { site: { keywords: keywords.content.replace(/\s/g, '') } }); } +function setDeviceSua(hints) { + let data = Array.isArray(hints) && hints.length === 0 + ? GreedyPromise.resolve(sua.le()) + : sua.he(hints); + return data.then((sua) => { + if (sua != null) { + mergeDeep(ortb2, {device: {sua}}); + } + }) +} + /** * Resets modules global ortb2 data */ export const resetEnrichments = () => { ortb2 = null }; -function runEnrichments() { +function runEnrichments(fpdConf) { setReferer(); setPage(); setDomain(); setDimensions(); setKeywords(); - - return ortb2; + return setDeviceSua(fpdConf.uaHints).then(() => ortb2); } export function processFpd(fpdConf, {global}) { if (fpdConf.skipEnrichments) { return {global}; } else { + let ready; if (ortb2 == null) { ortb2 = {}; - runEnrichments(); + ready = runEnrichments(fpdConf); + } else { + ready = GreedyPromise.resolve(); } - return { + return ready.then(() => ({ global: mergeDeep({}, ortb2, global) - }; + })) } } /** @type {firstPartyDataSubmodule} */ diff --git a/test/spec/fpd/sua_spec.js b/test/spec/fpd/sua_spec.js index b59f53a427f..121922fa78d 100644 --- a/test/spec/fpd/sua_spec.js +++ b/test/spec/fpd/sua_spec.js @@ -30,7 +30,8 @@ describe('uaDataToSUA', () => { const sua = uaDataToSUA(SUA_SOURCE_UNKNOWN, example); expect(sua.hasOwnProperty(suaKey)).to.be.false; }) - }) + }); + it('should convert low-entropy userAgentData', () => { const sua = uaDataToSUA(SUA_SOURCE_LOW_ENTROPY, { 'brands': [ @@ -176,6 +177,10 @@ describe('lowEntropySUAAccessor', () => { it('should return null if no uaData is available', () => { expect(getSUA(null)).to.eql(null); }) + + it('should return null if uaData is empty', () => { + expect(getSUA({})).to.eql(null); + }) }); describe('highEntropySUAAccessor', () => { @@ -213,6 +218,12 @@ describe('highEntropySUAAccessor', () => { expect(result).to.eql(null); }) }); + it('getHighEntropyValues returns an empty object', () => { + userAgentData.getHighEntropyValues.callsFake(() => Promise.resolve({})); + return getSUA().then((result) => { + expect(result).to.eql(null); + }); + }) }); it('should pass hints to userAgentData', () => { getSUA(['h1', 'h2']); diff --git a/test/spec/modules/enrichmentFpdModule_spec.js b/test/spec/modules/enrichmentFpdModule_spec.js index 4f5be56e6d4..afe116413e1 100644 --- a/test/spec/modules/enrichmentFpdModule_spec.js +++ b/test/spec/modules/enrichmentFpdModule_spec.js @@ -1,6 +1,8 @@ import { expect } from 'chai'; import { getRefererInfo } from 'src/refererDetection.js'; -import { processFpd, coreStorage, resetEnrichments } from 'modules/enrichmentFpdModule.js'; +import {processFpd, coreStorage, resetEnrichments} from 'modules/enrichmentFpdModule.js'; +import * as enrichmentModule from 'modules/enrichmentFpdModule.js'; +import {GreedyPromise} from '../../../src/utils/promise.js'; describe('the first party data enrichment module', function() { let width; @@ -11,6 +13,8 @@ describe('the first party data enrichment module', function() { let coreStorageStub; let canonical; let keywords; + let lowEntropySuaStub; + let highEntropySuaStub; before(function() { canonical = document.createElement('link'); @@ -36,6 +40,8 @@ describe('the first party data enrichment module', function() { .returns(null) // co.uk .onSecondCall() .returns('writeable'); // domain.co.uk + lowEntropySuaStub = sinon.stub(enrichmentModule.sua, 'le').callsFake(() => null); + highEntropySuaStub = sinon.stub(enrichmentModule.sua, 'he').callsFake(() => GreedyPromise.resolve()); }); afterEach(function() { @@ -47,13 +53,21 @@ describe('the first party data enrichment module', function() { canonical.rel = 'canonical'; keywords = document.createElement('meta'); keywords.name = 'keywords'; + lowEntropySuaStub.restore(); + highEntropySuaStub.restore(); }); + function syncProcessFpd(fpdConf, ortb2Fragments) { + let result; + processFpd(fpdConf, ortb2Fragments).then((res) => { result = res; }); + return result; + }; + it('adds ref and device values', function() { width = 800; height = 500; - let validated = processFpd({}, {}).global; + let validated = syncProcessFpd({}, {}).global; const {ref, page, domain} = getRefererInfo(); expect(validated.site.ref).to.equal(ref || undefined); @@ -68,7 +82,7 @@ describe('the first party data enrichment module', function() { height = 500; canonical.href = 'https://www.subdomain.domain.co.uk/path?query=12345'; - let validated = processFpd({}, {}).global; + let validated = syncProcessFpd({}, {}).global; expect(validated.site.ref).to.equal(getRefererInfo().ref || undefined); expect(validated.site.page).to.equal('https://www.subdomain.domain.co.uk/path?query=12345'); @@ -83,7 +97,7 @@ describe('the first party data enrichment module', function() { height = 500; keywords.content = 'value1,value2,value3'; - let validated = processFpd({}, {}).global; + let validated = syncProcessFpd({}, {}).global; expect(validated.site.keywords).to.equal('value1,value2,value3'); }); @@ -92,7 +106,7 @@ describe('the first party data enrichment module', function() { width = 800; height = 500; - let validated = processFpd({}, {global: {device: {w: 1200, h: 700}, site: {ref: 'https://someUrl.com', page: 'test.com'}}}).global; + let validated = syncProcessFpd({}, {global: {device: {w: 1200, h: 700}, site: {ref: 'https://someUrl.com', page: 'test.com'}}}).global; expect(validated.site.ref).to.equal('https://someUrl.com'); expect(validated.site.page).to.equal('test.com'); @@ -103,9 +117,26 @@ describe('the first party data enrichment module', function() { it('does not run enrichments again on the second call', () => { width = 1; height = 2; - const first = processFpd({}, {}).global; + const first = syncProcessFpd({}, {}).global; width = 3; - const second = processFpd({}, {}).global; + const second = syncProcessFpd({}, {}).global; expect(first).to.eql(second); + }); + + describe('device.sua', () => { + it('does not set device.sua if resolved sua is null', () => { + const sua = syncProcessFpd({}, {}).global.device?.sua; + expect(sua).to.not.exist; + }); + it('uses low entropy values if uaHints is []', () => { + lowEntropySuaStub.callsFake(() => ({mock: 'sua'})); + const sua = syncProcessFpd({uaHints: []}, {}).global.device.sua; + expect(sua).to.eql({mock: 'sua'}); + }); + it('uses high entropy values otherwise', () => { + highEntropySuaStub.callsFake((hints) => GreedyPromise.resolve({hints})); + const sua = syncProcessFpd({uaHints: ['h1', 'h2']}, {}).global.device.sua; + expect(sua).to.eql({hints: ['h1', 'h2']}); + }) }) }); diff --git a/test/spec/modules/fpdModule_spec.js b/test/spec/modules/fpdModule_spec.js index 6c138db30d3..8e95f462830 100644 --- a/test/spec/modules/fpdModule_spec.js +++ b/test/spec/modules/fpdModule_spec.js @@ -5,6 +5,7 @@ import {processFpd, registerSubmodules, startAuctionHook, reset} from 'modules/f import * as enrichmentModule from 'modules/enrichmentFpdModule.js'; import * as validationModule from 'modules/validationFpdModule/index.js'; import {resetEnrichments} from 'modules/enrichmentFpdModule.js'; +import {GreedyPromise} from '../../../src/utils/promise.js'; describe('the first party data module', function () { afterEach(function () { @@ -58,6 +59,8 @@ describe('the first party data module', function () { let querySelectorStub; let canonical; let keywords; + let lowEntropySuaStub; + let highEntropySuaStub; before(function() { reset(); @@ -81,6 +84,8 @@ describe('the first party data module', function () { heightStub = sinon.stub(window.top, 'innerHeight').get(function () { return height; }); + lowEntropySuaStub = sinon.stub(enrichmentModule.sua, 'le').callsFake(() => null); + highEntropySuaStub = sinon.stub(enrichmentModule.sua, 'he').callsFake(() => GreedyPromise.resolve()); }); afterEach(function() { @@ -91,6 +96,8 @@ describe('the first party data module', function () { canonical.rel = 'canonical'; keywords = document.createElement('meta'); keywords.name = 'keywords'; + lowEntropySuaStub.restore(); + highEntropySuaStub.restore(); }); it('filters ortb2 data that is set', function () {