├── .esdoc.json ├── jest.config.js ├── .github ├── issue_template.md └── pull_request_template.md ├── jestPreprocess.js ├── .circleci └── config.yml ├── src ├── util │ ├── debounce.js │ ├── customTargeting.js │ ├── log.js │ ├── query.js │ ├── resources.js │ ├── polyfills.js │ └── mobile.js ├── __tests__ │ ├── query.test.js │ ├── amazon.test.js │ ├── resources.test.js │ ├── util.test.js │ ├── prebid.test.js │ ├── sizemapping.test.js │ ├── headerbidding.test.js │ ├── arcads.test.js │ ├── gpt.test.js │ ├── displayAd.test.js │ ├── registerAds.test.js │ └── mobile.test.js ├── services │ ├── amazon.js │ ├── prebid.js │ ├── gpt.js │ ├── headerbidding.js │ └── sizemapping.js └── index.js ├── .eslintrc.js ├── .gitignore ├── LICENSE ├── webpack.config.js ├── debugging.js ├── CONTRIBUTING.md ├── package.json ├── dist └── arcads.js └── README.md /.esdoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "source": "./src", 3 | "destination": "./docs", 4 | "plugins": [{"name": "esdoc-standard-plugin"}] 5 | } 6 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | testURL: "http://localhost/", 3 | transform: { '^.+\\.js$': '/jestPreprocess.js' } 4 | } 5 | -------------------------------------------------------------------------------- /.github/issue_template.md: -------------------------------------------------------------------------------- 1 | ### Expected Behavior 2 | 3 | ### Actual Behavior 4 | 5 | ### Steps to Reproduce the Behavior 6 | 7 | ### Additional Comments 8 | -------------------------------------------------------------------------------- /jestPreprocess.js: -------------------------------------------------------------------------------- 1 | const babelOptions = { 2 | presets: ['env', 'stage-2'], 3 | plugins: ['transform-decorators-legacy', 'babel-plugin-root-import'], 4 | }; 5 | 6 | module.exports = require('babel-jest').createTransformer(babelOptions) 7 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ## On this branch 2 | _Include a description of changes introduced on branch here_ 3 | 4 | 5 | #### Verify 6 | 7 | - [ ] Confirm that cross browser testing has been completed. 8 | 9 | - [ ] Verify that no errors are present in the GPT console `window.googletag.openConsole()`. 10 | 11 | 12 | #### Comments 13 | _Include any comments to help with an effective code review here_ 14 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | jobs: 3 | build: 4 | machine: true 5 | working_directory: ~/arcads 6 | steps: 7 | - checkout 8 | - restore_cache: 9 | keys: 10 | - v1-dependencies-{{ checksum "package.json" }} 11 | - v1-dependencies- 12 | - run: npm install 13 | - save_cache: 14 | paths: 15 | - node_modules 16 | key: v1-dependencies-{{ checksum "package.json" }} 17 | - run: npm test 18 | -------------------------------------------------------------------------------- /src/util/debounce.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @desc Debounces a function preventing it from running more then every so many milliseconds. Useful for scroll or resize handlers. 3 | * @param {function} func - The function that should be debounced. 4 | * @param {number} wait - The amount of time a function should wait before it fires again. 5 | * @return - Returns a function every so many milliseconds based on the provided parameters. 6 | **/ 7 | export function debounce(func, wait) { 8 | let timeout; 9 | return function (...args) { 10 | clearTimeout(timeout); 11 | timeout = setTimeout(() => { 12 | timeout = null; 13 | func.apply(this, args); 14 | }, wait); 15 | }; 16 | } 17 | -------------------------------------------------------------------------------- /src/util/customTargeting.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @desc If a different key is required to serve position targeting for older creatives, rename it here. 3 | * @param {object} targeting - Targeting object passed in from the ad object. 4 | * @param {number} positionValue - The nth number of adType included. 5 | * @return - Returns the targeting object with the old position value stripped out, and the new one with the desired key in its place. 6 | **/ 7 | export function renamePositionKey(targeting, positionValue) { 8 | const newTargetingObject = targeting; 9 | const keyName = targeting.position.as; 10 | delete newTargetingObject.position; 11 | newTargetingObject[keyName] = positionValue; 12 | Object.assign(targeting, newTargetingObject); 13 | return targeting; 14 | } 15 | -------------------------------------------------------------------------------- /src/util/log.js: -------------------------------------------------------------------------------- 1 | import anylogger from 'anylogger'; 2 | import 'anylogger-console'; 3 | /** 4 | * @desc Determines whether or not to log based on a url param. Takes description as a parameter and returns log. 5 | * @param {string} description - The description that should go in the log. 6 | **/ 7 | export function sendLog(parentFunc, description, slotName) { 8 | try { 9 | if ((new URLSearchParams(window.location.search)).get('debug') === 'true') { 10 | const log = anylogger('arcads.js'); 11 | log({ 12 | service: 'ArcAds', 13 | timestamp: `${new Date()}`, 14 | 'logging from': parentFunc, 15 | description, 16 | slotName 17 | }); 18 | } 19 | } catch (error) { 20 | console.error(error); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/util/query.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @desc Accepts a key as a string and returns the value of a query parameter on the page request. 3 | * @param {string} param - A string that represents the key of a query paramter, for example 'adslot' will return 'hello' if the url has '?adslot=hello' at the end of it. 4 | * @return - Returns a string containing the value of a query paramter. 5 | **/ 6 | export function expandQueryString(param) { 7 | const url = window.location.href; 8 | const name = param.replace(/[[\]]/g, '\\$&'); 9 | const regex = new RegExp(`[?&]${name}(=([^&#]*)|&|#|$)`); 10 | const results = regex.exec(url); 11 | 12 | if (!results) { 13 | return null; 14 | } 15 | 16 | if (!results[2]) { 17 | return ''; 18 | } 19 | return decodeURIComponent(results[2].replace(/\+/g, ' ')); 20 | } 21 | -------------------------------------------------------------------------------- /src/util/resources.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @desc Appends a remote resource to the page within a HTML tag. 3 | * @param {string} tagname - A string containing the type of HTML tag that should be appended. 4 | * @param {string} url - A string containing the path of the resource. 5 | * @param {boolean} async - A boolean representing if the resource should be loaded asynchronously or not. 6 | * @param {boolean} defer - A boolean representing if the resource should be deferred or not. 7 | * @param {function} cb - An optional callback function that should fire whenever the resource has been appended. 8 | **/ 9 | export function appendResource(tagname, url, async, defer, cb) { 10 | const tag = document.createElement(tagname); 11 | if (tagname === 'script') { 12 | tag.src = url; 13 | tag.async = async || false; 14 | tag.defer = async || defer || false; 15 | } else { 16 | return; 17 | } 18 | (document.head || document.documentElement).appendChild(tag); 19 | 20 | if (cb) { 21 | cb(); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/__tests__/query.test.js: -------------------------------------------------------------------------------- 1 | import { expandQueryString } from '../util/query.js'; 2 | 3 | 4 | describe('expandQueryString', () => { 5 | 6 | const saveLocation = global.window.location; 7 | 8 | afterAll(() => { 9 | delete global.window.location; 10 | global.window.location = saveLocation; 11 | }); 12 | 13 | it('gets url value for param name passed', () => { 14 | delete global.window.location; 15 | global.window = Object.create(window); 16 | global.window.location = { 17 | href:'http://www.test.com?adslot=hello', 18 | }; 19 | 20 | const result = expandQueryString('adslot'); 21 | expect(result).toEqual('hello'); 22 | }); 23 | 24 | it('if no result return empty string', () => { 25 | delete global.window.location; 26 | global.window = Object.create(window); 27 | global.window.location = { 28 | href:'http://www.test.com?adslot=', 29 | }; 30 | 31 | const result = expandQueryString('adslot'); 32 | expect(result).toEqual(''); 33 | }); 34 | }); -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | commonjs: true, 5 | es6: true, 6 | node: true 7 | }, 8 | extends: 'airbnb-base', 9 | parserOptions: { 10 | sourceType: 'module', 11 | ecmaFeatures: { 12 | jsx: true 13 | } 14 | }, 15 | rules: { 16 | 'no-console': 0, 17 | 'class-methods-use-this': 0, 18 | 'import/prefer-default-export': 0, 19 | 'import/no-named-as-default': 0, 20 | 'import/no-extraneous-dependencies': 0, 21 | 'func-names': 0, 22 | 'prefer-arrow-callback': 0, 23 | 'consistent-return': 0, 24 | "object-curly-newline": [ 25 | "error", 26 | { 27 | "minProperties": 3, 28 | "multiline": true, 29 | } 30 | ], 31 | "spaced-comment": 0, 32 | "comma-dangle": 0, 33 | "semi": 2, 34 | "no-prototype-builtins": 0, 35 | "object-curly-newline": 0, 36 | "no-restricted-syntax": 0, 37 | "max-len": 0, 38 | "no-plusplus": 0, 39 | "no-undef": 0, 40 | "arrow-body-style": 0, 41 | "no-use-before-define": 0, 42 | "radix": 0 43 | } 44 | }; -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # Bower dependency directory (https://bower.io/) 24 | bower_components 25 | 26 | # node-waf configuration 27 | .lock-wscript 28 | 29 | # Compiled binary addons (http://nodejs.org/api/addons.html) 30 | build/Release 31 | 32 | # Dependency directories 33 | node_modules/ 34 | jspm_packages/ 35 | 36 | # Typescript v1 declaration files 37 | typings/ 38 | 39 | # Optional npm cache directory 40 | .npm 41 | 42 | # Optional eslint cache 43 | .eslintcache 44 | 45 | # Optional REPL history 46 | .node_repl_history 47 | 48 | # Output of 'npm pack' 49 | *.tgz 50 | 51 | # Yarn Integrity file 52 | .yarn-integrity 53 | 54 | # dotenv environment variables file 55 | .env 56 | 57 | # Docs 58 | docs 59 | 60 | # Misc 61 | .DS_Store -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 The Washington Post 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/__tests__/amazon.test.js: -------------------------------------------------------------------------------- 1 | import { 2 | fetchAmazonBids, 3 | queueAmazonCommand, 4 | } from '../services/amazon'; 5 | 6 | describe('amazon service', () => { 7 | beforeEach(() => { 8 | global.amazonTest = { 9 | cmd: () => jest.fn().mockName('cmd'), 10 | queueAmazonCommand: () => jest.fn().mockName('queueAmazonCommand'), 11 | }; 12 | }); 13 | 14 | afterEach(() => { 15 | window.apstag = false; 16 | jest.restoreAllMocks(); 17 | }); 18 | 19 | it('fetchAmazonBids', () => { 20 | window.innerWidth = '1280'; 21 | window.apstag = { 22 | fetchBids: () => jest.fn(), 23 | }; 24 | const mockSpy = jest.spyOn(window.apstag, 'fetchBids'); 25 | fetchAmazonBids( 26 | 'id', 'slotname', 27 | [ 28 | [[728, 90], 90], 29 | [728, 90], 30 | [468, 60] 31 | ], [[728, 90], 1080] 32 | ); 33 | expect(mockSpy).toHaveBeenCalled(); 34 | }); 35 | 36 | it('call passed in function in queueAmazonCommand', () => { 37 | window.apstag = true; 38 | const spy = jest.spyOn(amazonTest, 'cmd'); 39 | queueAmazonCommand(global.amazonTest.cmd); 40 | expect(spy).toHaveBeenCalled(); 41 | }); 42 | }); -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const UglifyJsPlugin = require('uglifyjs-webpack-plugin'); 2 | const path = require('path'); 3 | 4 | const generatePlugins = function (env) { 5 | const plugins = []; 6 | if (env.production) { 7 | plugins.push(new UglifyJsPlugin({ 8 | sourceMap: true, 9 | })); 10 | } 11 | return plugins; 12 | }; 13 | 14 | module.exports = env => ({ 15 | entry: './src/index.js', 16 | output: { 17 | path: path.resolve(__dirname, 'dist'), 18 | filename: 'arcads.js', 19 | libraryTarget: 'umd', 20 | }, 21 | devtool: env.development ? 'inline-source-map' : false, 22 | resolve: { extensions: ['.js', '.json'] }, 23 | module: { 24 | rules: [ 25 | { 26 | loader: 'eslint-loader', 27 | enforce: 'pre', 28 | test: /\.js$/, 29 | exclude: /node_modules/, 30 | options: { configFile: '.eslintrc.js' }, 31 | }, 32 | { 33 | test: /\.js$/, 34 | exclude: /node_modules/, 35 | use: { 36 | loader: 'babel-loader', 37 | options: { 38 | presets: ['env'], 39 | plugins: ['transform-decorators-legacy', 'transform-object-rest-spread'], 40 | }, 41 | }, 42 | }, 43 | ], 44 | }, 45 | plugins: generatePlugins(env), 46 | }); -------------------------------------------------------------------------------- /src/util/polyfills.js: -------------------------------------------------------------------------------- 1 | import Promise from 'promise-polyfill'; 2 | 3 | if (!window.Promise) { 4 | window.Promise = Promise; 5 | } 6 | 7 | /* eslint-disable */ 8 | 9 | // source: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/assign#Polyfill 10 | /* Object.assign() for IE11 (obviously) */ 11 | if (typeof Object.assign != 'function') { 12 | // Must be writable: true, enumerable: false, configurable: true 13 | Object.defineProperty(Object, "assign", { 14 | value: function assign(target, varArgs) { // .length of function is 2 15 | 'use strict'; 16 | if (target == null) { // TypeError if undefined or null 17 | throw new TypeError('Cannot convert undefined or null to object'); 18 | } 19 | 20 | var to = Object(target); 21 | 22 | for (var index = 1; index < arguments.length; index++) { 23 | var nextSource = arguments[index]; 24 | 25 | if (nextSource != null) { // Skip over if undefined or null 26 | for (var nextKey in nextSource) { 27 | // Avoid bugs when hasOwnProperty is shadowed 28 | if (Object.prototype.hasOwnProperty.call(nextSource, nextKey)) { 29 | to[nextKey] = nextSource[nextKey]; 30 | } 31 | } 32 | } 33 | } 34 | return to; 35 | }, 36 | writable: true, 37 | configurable: true 38 | }); 39 | } 40 | 41 | /* eslint-enable */ 42 | -------------------------------------------------------------------------------- /debugging.js: -------------------------------------------------------------------------------- 1 | const http = require('http') 2 | const url = require('url') 3 | const fs = require('fs') 4 | const path = require('path') 5 | const port = process.argv[2] || 9000 6 | 7 | http.createServer(function (req, res) { 8 | console.log(`${req.method} ${req.url}`); 9 | 10 | const parsedUrl = url.parse(req.url); 11 | let pathname = `.${parsedUrl.pathname}`; 12 | const ext = path.parse(pathname).ext; 13 | const map = { 14 | '.ico': 'image/x-icon', 15 | '.html': 'text/html', 16 | '.js': 'text/javascript', 17 | '.json': 'application/json', 18 | '.css': 'text/css', 19 | '.png': 'image/png', 20 | '.jpg': 'image/jpeg', 21 | '.wav': 'audio/wav', 22 | '.mp3': 'audio/mpeg', 23 | '.svg': 'image/svg+xml', 24 | '.pdf': 'application/pdf', 25 | '.doc': 'application/msword' 26 | }; 27 | 28 | fs.exists(pathname, (exist) => { 29 | if(!exist) { 30 | res.statusCode = 404; 31 | res.end(`File ${pathname} not found!`); 32 | return 33 | } 34 | 35 | if (fs.statSync(pathname).isDirectory()) pathname += `/index${ext}`; 36 | 37 | fs.readFile(pathname, (err, data) => { 38 | if (err) { 39 | res.statusCode = 500; 40 | res.end(`Error getting the file: ${err}.`); 41 | } else { 42 | res.setHeader('Content-type', map[ext] || 'text/plain'); 43 | res.end(data); 44 | } 45 | }) 46 | }) 47 | }).listen(parseInt(port)); 48 | 49 | console.log(`Server listening on port ${port}`); 50 | 51 | -------------------------------------------------------------------------------- /src/__tests__/resources.test.js: -------------------------------------------------------------------------------- 1 | import {appendResource} from '../util/resources.js'; 2 | 3 | describe('appendResource', () => { 4 | const cbMock = jest.fn(); 5 | const appendChildMock = jest.fn(); 6 | const saveDocument = global.document; 7 | 8 | afterAll(() => { 9 | delete global.document; 10 | global.document = saveDocument; 11 | }); 12 | 13 | beforeAll(() => { 14 | delete global.document; 15 | global.document = { 16 | documentElement:{ 17 | appendChild: appendChildMock, 18 | }, 19 | createElement: jest.fn().mockReturnValue({}), 20 | } 21 | }); 22 | 23 | afterEach(() => { 24 | jest.clearAllMocks(); 25 | }); 26 | 27 | it('if tag is script create tag and append child tag', () => { 28 | appendResource('script', 'www.test.com', true, true, cbMock); 29 | expect(appendChildMock).toHaveBeenCalledTimes(1); 30 | const expectedParams = {"async": true, "defer": true, "src": "www.test.com"}; 31 | expect(appendChildMock).toHaveBeenCalledWith(expectedParams); 32 | expect(cbMock).toHaveBeenCalledTimes(1); 33 | }); 34 | 35 | it('if tag is not script do nothing', () => { 36 | appendResource('div', 'www.test.com', true, true, cbMock); 37 | expect(appendChildMock).toHaveBeenCalledTimes(0); 38 | expect(cbMock).toHaveBeenCalledTimes(0); 39 | }); 40 | 41 | it('if no async or defer assume false for those values', () => { 42 | appendResource('script', 'www.test.com', null,null , cbMock); 43 | expect(appendChildMock).toHaveBeenCalledTimes(1); 44 | const expectedParams = {"async": false, "defer": false, "src": "www.test.com"}; 45 | expect(appendChildMock).toHaveBeenCalledWith(expectedParams); 46 | expect(cbMock).toHaveBeenCalledTimes(1); 47 | }); 48 | }); 49 | -------------------------------------------------------------------------------- /src/services/amazon.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @desc Fetches a bid for an advertisement based on which services are enabled on unit and the wrapper. 3 | * @param {string} id - A string containing the advertisement id corresponding to the div the advertisement will load into. 4 | * @param {string} slotName - A string containing the slot name of the advertisement, for instance '1234/adn.com/homepage'. 5 | * @param {array} dimensions - An array containing all of the applicable sizes the advertisement can use. 6 | * @param {function} cb - An optional callback function that should fire whenever the bidding has concluded. 7 | **/ 8 | export function fetchAmazonBids(id, slotName, dimensions, breakpoints, cb = null) { 9 | // pass in breakpoints array 10 | let sizeArray = dimensions; 11 | 12 | if (breakpoints && typeof window.innerWidth !== 'undefined' && dimensions[0][0][0] !== undefined) { 13 | const viewPortWidth = window.innerWidth; 14 | let useIndex = -1; 15 | const breakpointsLength = breakpoints.length; 16 | 17 | for (let ind = 0; ind < breakpointsLength; ind++) { 18 | if (viewPortWidth >= breakpoints[ind][0]) { 19 | useIndex = ind; 20 | break; 21 | } 22 | } 23 | 24 | sizeArray = dimensions[useIndex]; 25 | } 26 | 27 | queueAmazonCommand(() => { 28 | const slot = { 29 | slotName, 30 | slotID: id, 31 | sizes: sizeArray 32 | }; 33 | 34 | // Retrieves the bid from Amazon 35 | window.apstag.fetchBids({ slots: [slot] }, () => { 36 | // Sets the targeting values on the display bid from apstag 37 | window.apstag.setDisplayBids(); 38 | if (cb) { 39 | cb(); 40 | } 41 | }); 42 | }); 43 | } 44 | 45 | /** 46 | * @desc Adds an Amazon command to a callback queue which awaits for window.apstag 47 | * @param {string} cmd - The function that should wait for window.apstag to be ready. 48 | **/ 49 | export function queueAmazonCommand(cmd) { 50 | if (window.apstag) { 51 | cmd(); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to ArcAds 2 | 3 | Welcome to the contribution guide for ArcAds. Here are some important resources to get you started: 4 | 5 | * [ArcAds Wiki](https://github.com/washingtonpost/ArcAds/wiki) 6 | * [Doubleclick for Publishers](http://www.google.com/dfp) 7 | * [Prebid.js](http://prebid.org/) 8 | * [Amazon A9](https://www.a9.com/) 9 | 10 | ## Questions or Issues 11 | If you have a general question or an issue please refer to our [Frequently Asked Questions](https://github.com/washingtonpost/ArcAds/wiki/Frequently-Asked-Questions) section which we'll update reguarly with questions. If you can't find an answer to your question please open an [issue](https://github.com/washingtonpost/ArcAds/issues). 12 | 13 | ## Testing 14 | ArcAds has a series of unit tests built with [Jest](https://facebook.github.io/jest/). We are always looking to improve our testing coverage , and request that if you create new functionality that you also include tests along with it. 15 | 16 | ## Submitting changes 17 | Please send a [Pull Request to ArcAds](https://github.com/washingtonpost/ArcAds/pull/new/master) with a clear list of what you've done (you read more about [Github pull requests here](http://help.github.com/pull-requests/)). 18 | Please follow the best practices guide below and make sure all of your commits are atomic (one feature per commit). 19 | 20 | Always write a clear log message for your commits. One-line messages are fine for small changes, but bigger changes should look like this: 21 | 22 | $ git commit -m "A brief summary of the commit 23 | > 24 | > A paragraph describing what changed and its impact." 25 | 26 | ## Best Practices 27 | 28 | * ArcAds gets linted with the [airbnb style guide](https://github.com/airbnb/javascript). 29 | * Code should be documented and commented using the [ESDoc](https://esdoc.org/) conventions. 30 | * Please make sure you've tested your code prior to submitting a pull request and ensure everything works. 31 | 32 | 33 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "arcads", 3 | "version": "4.0.1", 4 | "description": "ArcAds is a GPT wrapper created by Arc Publishing with publishers in mind.", 5 | "main": "dist/arcads.js", 6 | "scripts": { 7 | "test": "jest --config jest.config.js --no-cache --coverage", 8 | "build": "rm -rf dist && webpack --env.production", 9 | "dev": "rm -rf dist && webpack --env.development --watch", 10 | "docs": "rm -rf docs && ./node_modules/.bin/esdoc && open docs/index.html", 11 | "debug": "node debugging.js", 12 | "mock": "webpack-dev-server --progress --hot --inline && npm run test" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "git+https://github.com/washingtonpost/arcads.git" 17 | }, 18 | "keywords": [ 19 | "ads", 20 | "advertisements", 21 | "dfp", 22 | "gpt", 23 | "prebid", 24 | "amazon", 25 | "headerbidding", 26 | "hb" 27 | ], 28 | "author": "Arc", 29 | "license": "MIT", 30 | "bugs": { 31 | "url": "https://github.com/washingtonpost/arcads/issues" 32 | }, 33 | "devDependencies": { 34 | "@babel/core": "^7.0.0-beta.37", 35 | "babel-core": "^6.26.0", 36 | "babel-loader": "^7.1.2", 37 | "babel-plugin-root-import": "^5.1.0", 38 | "babel-plugin-transform-decorators-legacy": "^1.3.4", 39 | "babel-preset-env": "^1.6.1", 40 | "babel-preset-stage-2": "^6.24.1", 41 | "eslint": "^4.15.0", 42 | "eslint-config-airbnb": "^16.1.0", 43 | "eslint-config-standard": "^11.0.0-beta.0", 44 | "eslint-import-resolver-webpack": "^0.8.4", 45 | "eslint-loader": "^1.9.0", 46 | "eslint-plugin-import": "^2.8.0", 47 | "jest": "^22.0.4", 48 | "uglifyjs-webpack-plugin": "^1.1.6", 49 | "webpack": "^3.10.0", 50 | "webpack-cli": "^3.0.8" 51 | }, 52 | "dependencies": { 53 | "anylogger": "^1.0.10", 54 | "anylogger-console": "^1.0.0", 55 | "esdoc": "^1.0.4", 56 | "esdoc-standard-plugin": "^1.0.0", 57 | "promise-polyfill": "^8.0.0" 58 | }, 59 | "homepage": "https://github.com/washingtonpost/arcads#readme" 60 | } 61 | -------------------------------------------------------------------------------- /src/__tests__/util.test.js: -------------------------------------------------------------------------------- 1 | import anylogger from 'anylogger'; 2 | import 'anylogger-console'; 3 | import { renamePositionKey } from '../util/customTargeting'; 4 | import {debounce} from '../util/debounce.js'; 5 | import {sendLog} from '../util/log.js'; 6 | 7 | describe('The CustomTargeting.js functions', () => { 8 | it('should take targeting and position value, and rename the key as posn', () => { 9 | const targeting = { 10 | position: { 11 | as: 'posn' 12 | } 13 | }; 14 | 15 | const positionValue = 2; 16 | const updatedTargeting = renamePositionKey(targeting, positionValue); 17 | const newTargeting = { 18 | posn: positionValue 19 | }; 20 | 21 | expect(updatedTargeting).toEqual(newTargeting); 22 | }); 23 | }); 24 | 25 | describe('debounce', () => { 26 | beforeEach(() => { 27 | jest.useFakeTimers(); 28 | }); 29 | 30 | test('debounce', () => { 31 | const func = jest.fn(); 32 | const debouncedFunc = debounce(func, 1000); 33 | 34 | // Call debounced function immediately 35 | debouncedFunc(); 36 | expect(func).toHaveBeenCalledTimes(0); 37 | 38 | // Call debounced function several times with 500ms between each call 39 | for (let i = 0; i < 10; i += 1) { 40 | setTimeout(() => {}, 500); 41 | debouncedFunc(); 42 | } 43 | 44 | // Verify debounced function was not called yet 45 | expect(func).toHaveBeenCalledTimes(0); 46 | 47 | // Fast forward time 48 | jest.runAllTimers(); 49 | 50 | // Verify debounced function was only called once 51 | expect(func).toHaveBeenCalledTimes(1); 52 | }); 53 | }); 54 | 55 | describe('sendLog', () => { 56 | 57 | test('sendLog', () => { 58 | const location = { 59 | ...window.location, 60 | search: '?debug=true' 61 | }; 62 | 63 | Object.defineProperty(window, 'location', { 64 | writable: true, 65 | value: location 66 | }); 67 | 68 | const DATE_TO_USE = new Date('Thu Feb 04 2021 11:04:05 GMT-0500'); 69 | global.Date = jest.fn(() => DATE_TO_USE); 70 | anylogger.log = jest.fn(); 71 | sendLog('testFunc()', 'a test of the send log', null); 72 | setTimeout(() => { 73 | expect(anylogger.log).toHaveBeenCalledWith('arcads.js', [{"description": "a test of the send log", "logging from": "testFunc()", "service": "ArcAds", "slotName": null, "timestamp": "Thu Feb 04 2021 11:04:05 GMT-0500 (Eastern Standard Time)"}]); 74 | }, 500); 75 | }); 76 | 77 | test('sendLog if window undefined', () => { 78 | delete global.window; 79 | sendLog('testFunc()', 'a test of the send log', null); 80 | setTimeout(() => { 81 | expect(console.error).toHaveBeenCalled(); 82 | }, 500); 83 | }); 84 | }); 85 | -------------------------------------------------------------------------------- /src/__tests__/prebid.test.js: -------------------------------------------------------------------------------- 1 | import { 2 | queuePrebidCommand, 3 | fetchPrebidBids, 4 | addUnit, 5 | fetchPrebidBidsArray, 6 | } from '../services/prebid'; 7 | 8 | const setpbjs = () => { 9 | global.pbjs = { 10 | que: [], 11 | addAdUnits: () => jest.fn().mockName('addAdUnits'), 12 | requestBids: () => jest.fn().mockName('requestBids').mockReturnValueOnce('result'), 13 | setConfig: () => jest.fn().mockName('setConfig'), 14 | bidsBackHandler: () => jest.fn().mockName('bidsBackHandler'), 15 | setTargetingForGPTAsync: jest.fn().mockName('setTargetingForGPTAsync'), 16 | }; 17 | }; 18 | 19 | describe('pbjs', () => { 20 | const info = { 21 | bids: [], 22 | }; 23 | 24 | beforeEach(() => { 25 | setpbjs(); 26 | }); 27 | 28 | afterEach(() => { 29 | window.blockArcAdsPrebid = false; 30 | global.pbjs = {}; 31 | jest.restoreAllMocks(); 32 | }); 33 | 34 | it('return if blockArcAdsPrebid is block', () => { 35 | window.blockArcAdsPrebid = true; 36 | const spy = jest.spyOn(pbjs, 'requestBids'); 37 | const mockCb = jest.fn(); 38 | fetchPrebidBidsArray({}, {}, {}, info, {}, mockCb()); 39 | expect(spy).not.toHaveBeenCalled(); 40 | }); 41 | 42 | it('fetchPrebidBidsArray if blockArcAdsPrebid is not block', () => { 43 | const spy = jest.spyOn(pbjs, 'requestBids'); 44 | const mockCb = jest.fn(); 45 | fetchPrebidBidsArray({}, {}, {}, info, {}, mockCb()); 46 | expect(spy).toHaveBeenCalled(); 47 | }); 48 | 49 | it('fetchPrebidBids', () => { 50 | const spy = jest.spyOn(pbjs, 'requestBids'); 51 | fetchPrebidBids({}, {}, {}, info, {}); 52 | expect(spy).toHaveBeenCalled(); 53 | }); 54 | 55 | it('return undefined while window blockArcAdsPrebid is set to true', () => { 56 | window.blockArcAdsPrebid = true; 57 | const result = fetchPrebidBids({}, {}, {}, info, {}); 58 | expect(result).toEqual(undefined); 59 | }); 60 | 61 | it('push fn into queuePrebidCommand', () => { 62 | const mockFn = jest.fn(); 63 | queuePrebidCommand(mockFn); 64 | expect(pbjs.que.length).toEqual(1); 65 | }); 66 | 67 | it('addUnit while slot is available', () => { 68 | const spy = jest.spyOn(pbjs, 'addAdUnits'); 69 | addUnit('', '', {}, {}, {}); 70 | expect(spy).toHaveBeenCalled(); 71 | }); 72 | 73 | it('called config while sizeConfig is passed ', () => { 74 | const spy = jest.spyOn(pbjs, 'setConfig'); 75 | addUnit('', '', {}, { config: { sample: 'test' } }, {}); 76 | expect(spy).toHaveBeenCalled(); 77 | }); 78 | 79 | it('called setConfig while sizeConfig is passed ', () => { 80 | const spy = jest.spyOn(pbjs, 'setConfig'); 81 | addUnit('', '', {}, { sizeConfig: { sample: 'test' } }, {}); 82 | expect(spy).toHaveBeenCalled(); 83 | }); 84 | }); -------------------------------------------------------------------------------- /src/__tests__/sizemapping.test.js: -------------------------------------------------------------------------------- 1 | import { 2 | prepareSizeMaps, 3 | parseSizeMappings, 4 | runResizeEvents, 5 | setResizeListener, 6 | sizemapListeners, 7 | } from '../services/sizemapping'; 8 | import { fetchBids } from '../services/headerbidding'; 9 | const mockSizeMap = [[468, 60], [728, 90]]; 10 | describe('prepareSizeMaps', () => { 11 | it('return sizeMap object', () => { 12 | const mockDimensions = [[1000, 300], [970, 90], [728, 90], [300, 250]]; 13 | const result = prepareSizeMaps(mockDimensions, mockSizeMap); 14 | expect(result.mapping.length).toEqual(2); 15 | expect(result.breakpoints.length).toEqual(2); 16 | expect(result.correlators.length).toEqual(2); 17 | }); 18 | }); 19 | describe('parseSizeMappings', () => { 20 | Object.defineProperty(window, 'innerWidth', { 21 | writable: true, 22 | value: 1080, 23 | }); 24 | Object.defineProperty(window, 'innerHeight', { 25 | writable: true, 26 | value: 680, 27 | }); 28 | it('return sizeMap object', () => { 29 | const result = parseSizeMappings(mockSizeMap); 30 | expect(result).toEqual([]); 31 | }); 32 | }); 33 | describe('setResizeListener', () => { 34 | const mockParams = { 35 | id: '123', 36 | correlators: [1, 2] 37 | }; 38 | beforeEach(() => { 39 | global.window = { 40 | addEventListener: () => jest.fn().mockName('addEventListener'), 41 | }; 42 | }); 43 | it('return sizeMap object', () => { 44 | const mockSpy = jest.spyOn(global.window, 'addEventListener'); 45 | setResizeListener(mockParams); 46 | expect(mockSpy).toHaveBeenCalled(); 47 | }); 48 | }); 49 | describe('runResizeEvents', () => { 50 | beforeEach(() => { 51 | global.runResizeEvents = { 52 | fetchBids: () => jest.fn().mockName('fetchBids'), 53 | refreshSlot: () => jest.fn().mockName('fetchBids'), 54 | parseSizeMappings: () => jest.fn().mockName('parseSizeMappings'), 55 | }; 56 | Object.assign(sizemapListeners, { abc: { correlators: [1, 2, 3] } }); 57 | }); 58 | afterEach(() => { 59 | global = {}; 60 | }); 61 | const mockParams = { 62 | ad: {}, 63 | breakpoints: [768, 1080], 64 | id: 'abc', 65 | bidding: { 66 | prebid: { 67 | enabled: true, 68 | }, 69 | amazon: { 70 | enabled: false, 71 | } 72 | }, 73 | mapping: mockSizeMap, 74 | slotName: 'mockSlotName', 75 | wrapper: {}, 76 | prerender: false, 77 | }; 78 | it('set ad correlators to true', () => { 79 | Object.defineProperty(window, 'innerWidth', { 80 | writable: true, 81 | value: 1024, 82 | }); 83 | const result = runResizeEvents(mockParams); 84 | const resultFn = result(); 85 | expect(sizemapListeners.abc.correlators[0]).toEqual(true); 86 | }); 87 | }); -------------------------------------------------------------------------------- /src/services/prebid.js: -------------------------------------------------------------------------------- 1 | import { refreshSlot } from './gpt'; 2 | 3 | /** 4 | * @desc Queues a command inside of Prebid.js 5 | * @param {function} fn - Accepts a function to push into the Prebid command queue. 6 | **/ 7 | export function queuePrebidCommand(fn) { 8 | pbjs.que.push(fn); 9 | } 10 | 11 | /** 12 | * @desc Calls the Prebid request method for fetching bids, once fetched the advertisement is refreshed unless a callback is defined. 13 | * @param {object} ad - An object containing the GPT ad slot. 14 | * @param {string} code - A string containing the advertisement id or slotname corresponding to the div the advertisement will load into. 15 | * @param {number} timeout - An integer communicating how long in ms the Prebid.js service should wait before it closes the auction for a lot. 16 | * @param {object} info - An object containing information about the advertisement that is about to load. 17 | * @param {function} prerender - An optional function that will run before the advertisement renders. 18 | * @param {function} cb - An optional callback function that should fire whenever the bidding has concluded. 19 | **/ 20 | export function fetchPrebidBidsArray(ad, codes, timeout, info, prerender, cb = null) { 21 | pbjs.addAdUnits(info); //eslint-disable-line no-undef 22 | if (window.blockArcAdsPrebid) { 23 | return; 24 | } 25 | pbjs.requestBids({ 26 | timeout, 27 | adUnitCodes: codes, 28 | bidsBackHandler: (result) => { 29 | console.log('Bid Back Handler', result); 30 | pbjs.setTargetingForGPTAsync(codes); 31 | if (cb) { 32 | cb(); 33 | } else { 34 | refreshSlot({ ad, info, prerender }); 35 | } 36 | }, 37 | }); 38 | } 39 | 40 | export function fetchPrebidBids(ad, code, timeout, info, prerender, cb = null) { 41 | const newInfo = info; 42 | newInfo.bids = Array.isArray(info.bids) ? info.bids : [info.bids]; 43 | fetchPrebidBidsArray(ad, [code], timeout, newInfo, prerender, cb); 44 | } 45 | 46 | /** 47 | * @desc Registers an advertisement with Prebid.js so it's prepared to fetch bids for it. 48 | * @param {string} code - Contains the div id or slotname used for the advertisement 49 | * @param {array} sizes - An array of applicable ad sizes that are available for bidding. 50 | * @param {object} bids - Contains all of the applicable bid data, such as which vendors to use and their placement ids. 51 | * @param {object} wrapper - An object containing all enabled services on the Arc Ads. 52 | **/ 53 | export function addUnit(code, sizes, bids, wrapper = {}, others = {}) { 54 | // Formats the add unit for prebid.. 55 | const slot = { code, bids, ...others }; 56 | slot.mediaTypes = { banner: { sizes } }; 57 | const { sizeConfig, config } = wrapper; 58 | 59 | pbjs.addAdUnits(slot); 60 | 61 | if (config) { 62 | pbjs.setConfig(config); 63 | return; 64 | } 65 | 66 | if (sizeConfig) { 67 | pbjs.setConfig({ sizeConfig }); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/util/mobile.js: -------------------------------------------------------------------------------- 1 | /** @desc Utility class that determines the end user's browser user agent. **/ 2 | export class MobileDetection { 3 | /** 4 | * @desc Determines if the user is using an Android device. 5 | **/ 6 | static Android() { 7 | return !!navigator.userAgent.match(/Android/i); 8 | } 9 | 10 | /** 11 | * @desc Determines if the user is using an old Android device. 12 | **/ 13 | static AndroidOld() { 14 | return !!navigator.userAgent.match(/Android 2.3.3/i); 15 | } 16 | 17 | /** 18 | * @desc Determines if the user is using an Android tablet device. 19 | **/ 20 | static AndroidTablet() { 21 | return !!(navigator.userAgent.match(/Android/i) && !navigator.userAgent.match(/Mobile/i)); 22 | } 23 | 24 | /** 25 | * @desc Determines if the user is using a Kindle. 26 | **/ 27 | static Kindle() { 28 | return !!navigator.userAgent.match(/Kindle/i); 29 | } 30 | 31 | /** 32 | * @desc Determines if the user is using a Kindle Fire. 33 | **/ 34 | static KindleFire() { 35 | return !!navigator.userAgent.match(/KFOT/i); 36 | } 37 | 38 | /** 39 | * @desc Determines if the user is using Silk. 40 | **/ 41 | static Silk() { 42 | return !!navigator.userAgent.match(/Silk/i); 43 | } 44 | 45 | /** 46 | * @desc Determines if the user is using a BlackBerry device 47 | **/ 48 | static BlackBerry() { 49 | return !!navigator.userAgent.match(/BlackBerry/i); 50 | } 51 | 52 | /** 53 | * @desc Determines if the user is using an iOS device. 54 | **/ 55 | static iOS() { 56 | return !!navigator.userAgent.match(/iPhone|iPad|iPod/i); 57 | } 58 | 59 | /** 60 | * @desc Determines if the user is using an iPhone or iPod. 61 | **/ 62 | static iPhone() { 63 | return !!navigator.userAgent.match(/iPhone|iPod/i); 64 | } 65 | 66 | /** 67 | * @desc Determines if the user is using an iPad. 68 | **/ 69 | static iPad() { 70 | return !!navigator.userAgent.match(/iPad/i); 71 | } 72 | 73 | /** 74 | * @desc Determines if the user is using a Windows Mobile device. 75 | **/ 76 | static Windows() { 77 | return !!navigator.userAgent.match(/IEMobile/i); 78 | } 79 | 80 | /** 81 | * @desc Determines if the user is using FireFoxOS. 82 | **/ 83 | static FirefoxOS() { 84 | return !!navigator.userAgent.match(/Mozilla/i) && !!navigator.userAgent.match(/Mobile/i); 85 | } 86 | 87 | /** 88 | * @desc Determines if the user is using a Retina display. 89 | **/ 90 | static Retina() { 91 | return (window.retina || window.devicePixelRatio > 1); 92 | } 93 | 94 | /** 95 | * @desc Determines if the user is using any type of mobile device. 96 | **/ 97 | static any() { 98 | return (this.Android() || this.Kindle() || this.KindleFire() || this.Silk() || this.BlackBerry() || this.iOS() || this.Windows() || this.FirefoxOS()); 99 | } 100 | } 101 | 102 | export default MobileDetection; 103 | -------------------------------------------------------------------------------- /src/__tests__/headerbidding.test.js: -------------------------------------------------------------------------------- 1 | import { 2 | initializeBiddingServices 3 | } from '../services/headerbidding'; 4 | 5 | describe('initializeBiddingServices', function () { 6 | afterEach(() => { 7 | window.apstag = false; 8 | window.arcBiddingReady = true; 9 | jest.restoreAllMocks(); 10 | }); 11 | 12 | it('return while window.arcBiddingReady is already set in initializeBiddingServices', () => { 13 | Object.defineProperty(window, 'arcBiddingReady', { 14 | writable: true, 15 | value: true, 16 | }); 17 | const mockSetting = { 18 | prebid: false, 19 | amazon: false, 20 | }; 21 | const result = initializeBiddingServices(mockSetting); 22 | expect(result).toEqual(undefined); 23 | }); 24 | 25 | it('set arcBiddingReady to false while no prebid or amazon were set in initializeBiddingServices', () => { 26 | Object.defineProperty(window, 'arcBiddingReady', { 27 | writable: true, 28 | value: false, 29 | }); 30 | const mockSetting = { 31 | prebid: false, 32 | amazon: false, 33 | }; 34 | initializeBiddingServices(mockSetting); 35 | expect(window.arcBiddingReady).toEqual(false); 36 | }); 37 | 38 | it('enable prebid ', () => { 39 | Object.defineProperty(window, 'arcBiddingReady', { 40 | writable: true, 41 | value: false, 42 | }); 43 | Object.defineProperty(window, 'apstag', { 44 | writable: true, 45 | value: true, 46 | }); 47 | 48 | const mockSetting = { 49 | prebid: { 50 | enabled: true, 51 | }, 52 | amazon: { 53 | enabled: false, 54 | } 55 | }; 56 | 57 | initializeBiddingServices(mockSetting); 58 | expect(window.arcBiddingReady).toEqual(false); 59 | }); 60 | 61 | it('arcBiddingReady set to false while no pbjs available', () => { 62 | global.pbjs = undefined; 63 | const mockSetting = { 64 | prebid: { 65 | enabled: true, 66 | }, 67 | }; 68 | initializeBiddingServices(mockSetting); 69 | setTimeout(() => { 70 | expect(window.arcBiddingReady).toEqual(false); 71 | }, 2000); 72 | }); 73 | 74 | it('enable Amazon ', () => { 75 | Object.defineProperty(window, 'arcBiddingReady', { 76 | writable: true, 77 | value: false, 78 | }); 79 | Object.defineProperty(window, 'apstag', { 80 | writable: true, 81 | value: true, 82 | }); 83 | const mockSetting = { 84 | prebid: false, 85 | amazon: { 86 | enabled: true, 87 | id: 'mock-id' 88 | }, 89 | }; 90 | initializeBiddingServices(mockSetting); 91 | setTimeout(() => { 92 | expect(window.arcBiddingReady).toEqual(false); 93 | }, 2000); 94 | }); 95 | 96 | it('enable Amazon without id ', () => { 97 | Object.defineProperty(window, 'arcBiddingReady', { 98 | writable: true, 99 | value: false, 100 | }); 101 | Object.defineProperty(window, 'apstag', { 102 | writable: true, 103 | value: true, 104 | }); 105 | const mockSetting = { 106 | prebid: false, 107 | amazon: { 108 | enabled: true, 109 | id: '' 110 | }, 111 | }; 112 | initializeBiddingServices(mockSetting); 113 | 114 | setTimeout(() => { 115 | expect(window.arcBiddingReady).toEqual(false); 116 | }, 2000); 117 | }); 118 | }); -------------------------------------------------------------------------------- /src/services/gpt.js: -------------------------------------------------------------------------------- 1 | import { appendResource } from '../util/resources'; 2 | import { expandQueryString } from '../util/query'; 3 | import { sendLog } from '../util/log'; 4 | 5 | /** 6 | * @desc Initializes the Google Publisher tag scripts. 7 | **/ 8 | export function initializeGPT() { 9 | window.googletag = window.googletag || {}; 10 | window.googletag.cmd = window.googletag.cmd || []; 11 | 12 | appendResource('script', '//securepubads.g.doubleclick.net/tag/js/gpt.js', true, true); 13 | sendLog('initializeGPT()', 'Appended googletag script to the head tag of the page.', null); 14 | } 15 | 16 | /** 17 | * @desc Refreshes an advertisement via the GPT refresh method. If a prerender function is provided it is executed prior to the refresh. 18 | * @param {object} obj - An object containing all of the function arguments. 19 | * @param {Object} obj.ad - An object containing the GPT ad slot. 20 | * @param {boolean} obj.correlator - An optional boolean that describes if the correlator value should update or not. 21 | * @param {function} obj.prerender - An optional function that will run before the advertisement renders. 22 | * @param {object} obj.info - An object containing information about the advertisement that is about to load. 23 | **/ 24 | export function refreshSlot({ 25 | ad, 26 | correlator = false, 27 | prerender = null, 28 | info = {} 29 | }) { 30 | new Promise((resolve) => { 31 | if (prerender) { 32 | try { 33 | prerender(info).then(() => { 34 | resolve('Prerender function has completed.'); 35 | }); 36 | } catch (error) { 37 | console.warn(`ArcAds: Prerender function did not return a promise or there was an error. 38 | Documentation: https://github.com/washingtonpost/arcads/wiki/Utilizing-a-Prerender-Hook`); 39 | resolve('Prerender function did not return a promise or there was an error, ignoring.'); 40 | } 41 | } else { 42 | resolve('No Prerender function was provided.'); 43 | } 44 | }).then(() => { 45 | runRefreshEvent(); 46 | }); 47 | 48 | function runRefreshEvent() { 49 | if (window.blockArcAdsLoad) return 'blockArcAdsLoad'; 50 | if (window.googletag && googletag.pubadsReady) { 51 | window.googletag.pubads().refresh([ad], { changeCorrelator: correlator }); 52 | } else { 53 | setTimeout(() => { 54 | runRefreshEvent(); 55 | }, 200); 56 | } 57 | } 58 | } 59 | 60 | /** 61 | * @desc Queues a command inside of GPT. 62 | * @param {function} fn - Accepts a function to push into the Prebid command queue. 63 | **/ 64 | export function queueGoogletagCommand(fn) { 65 | window.googletag.cmd.push(fn); 66 | } 67 | 68 | /** 69 | * @desc Assigns key/value targeting to a specific advertisement. 70 | * @param {Object} ad - An object containing the GPT ad slot. 71 | * @param {Object} options - An object containing all of the key/value targeting pairs to assign to the advertisement. 72 | **/ 73 | export function setTargeting(ad, options) { 74 | for (const key in options) { 75 | if (options.hasOwnProperty(key) && options[key]) { 76 | ad.setTargeting(key, options[key]); 77 | } 78 | } 79 | } 80 | 81 | /** 82 | * @desc Configures the GPT configuration options. 83 | * @param {function} handleSlotRenderEnded - Callback function that gets fired whenever a GPT ad slot has finished rendering. 84 | **/ 85 | export function dfpSettings(handleSlotRenderEnded) { 86 | window.googletag.pubads().disableInitialLoad(); 87 | window.googletag.pubads().enableSingleRequest(); 88 | window.googletag.pubads().enableAsyncRendering(); 89 | 90 | if (this.collapseEmptyDivs) { 91 | sendLog('dfpSettings()', 'This wrapper is set to collapse any empty divs.', null); 92 | window.googletag.pubads().collapseEmptyDivs(); 93 | } 94 | window.googletag.enableServices(); 95 | 96 | if (handleSlotRenderEnded) { 97 | sendLog('dfpSettings()', 'This wrapper has a function to call upon the slot render ending.', null); 98 | window.googletag.pubads().addEventListener('slotRenderEnded', handleSlotRenderEnded); 99 | } 100 | } 101 | 102 | /** 103 | * @desc Determines the full slot name of the ad unit. If a user appends an 'adslot' query parameter to the page URL the slot name will be verridden. 104 | * @param {string} dfpCode - A string containing the publishers DFP id code. 105 | * @param {string} slotName - A string containing the slot name of the advertisement, for example 'homepage'. 106 | * @return - Returns a string combining the DFP id code and the slot name, for example '123/homepage'. 107 | **/ 108 | export function determineSlotName(dfpCode, slotName) { 109 | const slotOverride = expandQueryString('adslot'); 110 | if (slotOverride && (slotOverride !== '' || slotOverride !== null)) { 111 | return `/${dfpCode}/${slotOverride}`; 112 | } 113 | return `/${dfpCode}/${slotName}`; 114 | } 115 | -------------------------------------------------------------------------------- /src/services/headerbidding.js: -------------------------------------------------------------------------------- 1 | import { fetchPrebidBids, queuePrebidCommand } from './prebid'; 2 | import { fetchAmazonBids, queueAmazonCommand } from './amazon'; 3 | import { refreshSlot } from './gpt'; 4 | import { sendLog } from '../util/log'; 5 | 6 | /** 7 | * @desc Initializes all header bidding services and appends the applicable scripts to the page. 8 | * @param {object} obj - An object containing all of the function arguments. 9 | * @param {object} obj.prebid - An object containing configuration data for Prebid.js. 10 | * @param {object} obj.amazon - An object containing configuration data for Amazon A9 and TAM. 11 | **/ 12 | export function initializeBiddingServices({ 13 | prebid = false, 14 | amazon = false 15 | }) { 16 | if (window.arcBiddingReady) { 17 | sendLog('initializeBiddingServices()', 'Header bidding has been previously initialized', null); 18 | return; 19 | } 20 | 21 | window.arcBiddingReady = false; 22 | 23 | const enablePrebid = new Promise((resolve) => { 24 | if (prebid && prebid.enabled) { 25 | if (typeof pbjs === 'undefined') { 26 | const pbjs = pbjs || {}; 27 | pbjs.que = pbjs.que || []; 28 | } 29 | resolve('Prebid has been initialized'); 30 | } else { 31 | sendLog('initializeBiddingServices()', 'Prebid is not enabled on this wrapper.', null); 32 | resolve('Prebid is not enabled on the wrapper...'); 33 | } 34 | }); 35 | 36 | const enableAmazon = new Promise((resolve) => { 37 | if (amazon && amazon.enabled && window.apstag) { 38 | if (amazon.id && amazon.id !== '') { 39 | queueAmazonCommand(() => { 40 | // Initializes the Amazon APS tag script. 41 | window.apstag.init({ 42 | pubID: amazon.id, 43 | adServer: 'googletag' 44 | }); 45 | 46 | resolve('Amazon scripts have been added onto the page!'); 47 | }); 48 | } else { 49 | console.warn(`ArcAds: Missing Amazon account id. 50 | Documentation: https://github.com/washingtonpost/arcads#amazon-tama9`); 51 | sendLog('initializeBiddingServices()', 'Amazon is not enabled on this wrapper.', null); 52 | resolve('Amazon is not enabled on the wrapper...'); 53 | } 54 | } else { 55 | resolve('Amazon is not enabled on the wrapper...'); 56 | } 57 | }); 58 | 59 | // Waits for all header bidding services to be initialized before telling the service it's ready to retrieve bids. 60 | Promise.all([enablePrebid, enableAmazon]) 61 | .then(() => { 62 | window.arcBiddingReady = true; 63 | }); 64 | } 65 | 66 | /** 67 | * @desc Fetches a bid for an advertisement based on which services are enabled on unit and the wrapper. 68 | * @param {object} obj - An object containing all of the function arguments. 69 | * @param {Object} obj.ad - An object containing the GPT ad slot. 70 | * @param {string} obj.id - A string containing the advertisement id corresponding to the div the advertisement will load into. 71 | * @param {string} obj.slotName - A string containing the slot name of the advertisement, for instance '1234/adn.com/homepage'. 72 | * @param {Array} obj.dimensions - An array containing all of the applicable sizes the advertisement can use. 73 | * @param {Object} obj.wrapper - An object containing all of the wrapper settings. 74 | * @param {Array} obj.bidding - Contains all of the applicable bid data, such as which vendors to use and their placement ids. 75 | * @param {boolean} obj.correlator - An optional boolean that describes if the correlator value should update or not. 76 | * @param {function} obj.prerender - An optional function that will run before the advertisement renders. 77 | **/ 78 | export function fetchBids({ 79 | ad, 80 | id, 81 | slotName, 82 | dimensions, 83 | wrapper, 84 | bidding, 85 | correlator = false, 86 | prerender, 87 | breakpoints 88 | }) { 89 | const adInfo = { 90 | adUnit: ad, 91 | adSlot: slotName, 92 | adDimensions: dimensions, 93 | adId: id, 94 | bids: bidding, 95 | }; 96 | 97 | const prebidBids = new Promise((resolve) => { 98 | if (wrapper.prebid && wrapper.prebid.enabled) { 99 | const timeout = wrapper.prebid.timeout || 700; 100 | queuePrebidCommand.bind(this, fetchPrebidBids(ad, wrapper.prebid.useSlotForAdUnit ? slotName : id, timeout, adInfo, prerender, () => { 101 | resolve('Fetched Prebid ads!'); 102 | })); 103 | } else { 104 | resolve('Prebid is not enabled on the wrapper...'); 105 | } 106 | }); 107 | 108 | const amazonBids = new Promise((resolve) => { 109 | if (wrapper.amazon && wrapper.amazon.enabled) { 110 | fetchAmazonBids(id, slotName, dimensions, breakpoints, () => { 111 | resolve('Fetched Amazon ads!'); 112 | }); 113 | } else { 114 | resolve('Amazon is not enabled on the wrapper...'); 115 | } 116 | }); 117 | 118 | if (window.arcBiddingReady) { 119 | Promise.all([prebidBids, amazonBids]) 120 | .then(() => { 121 | refreshSlot({ 122 | ad, 123 | correlator, 124 | prerender, 125 | info: adInfo 126 | }); 127 | }); 128 | } else { 129 | setTimeout(() => initializeBiddingServices(), 200); 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /src/__tests__/arcads.test.js: -------------------------------------------------------------------------------- 1 | import { ArcAds } from '../index'; 2 | import * as gptService from '../services/gpt.js'; 3 | import * as prebidService from '../services/prebid.js'; 4 | 5 | describe('arcads', () => { 6 | 7 | beforeEach(() => { 8 | jest.clearAllMocks(); 9 | }); 10 | 11 | const arcAds = new ArcAds({ 12 | dfp: { 13 | id: '123' 14 | }, 15 | bidding: { 16 | amazon: { 17 | enabled: true, 18 | id: '123' 19 | }, 20 | prebid: { 21 | enabled: true 22 | } 23 | } 24 | }); 25 | 26 | let registerAdCollectionSingleCallMock; 27 | 28 | describe('constructor', () => { 29 | it('should initialize arc ads', () => { 30 | expect(arcAds).not.toBeUndefined(); 31 | }); 32 | 33 | it('should initialize googletag', () => { 34 | const { googletag } = global; 35 | expect(googletag).toBeDefined(); 36 | }); 37 | 38 | it('should initialize header bidding serivces', () => { 39 | const { arcBiddingReady } = global; 40 | expect(arcBiddingReady).toBeDefined(); 41 | }); 42 | 43 | it('should console warn if no dfpID provided', () => { 44 | const consoleMock = jest.fn(); 45 | console.warn = consoleMock; 46 | const arcAds = new ArcAds({ 47 | dfp: { 48 | } 49 | }); 50 | 51 | expect(consoleMock).toHaveBeenCalledTimes(1); 52 | expect(consoleMock).toHaveBeenCalledWith("ArcAds: DFP id is missing from the arcads initialization script.", '\n', 53 | "Documentation: https://github.com/washingtonpost/arcads#getting-started"); 54 | }); 55 | }); 56 | 57 | describe('registerAdCollection', () => { 58 | it('calls registerAd for each advert in the collection param', () => { 59 | 60 | const registerAdMock = jest.fn(); 61 | arcAds.registerAd = registerAdMock; 62 | 63 | const adCollection = ['ad1', 'ad2']; 64 | arcAds.registerAdCollection(adCollection); 65 | 66 | expect(registerAdMock).toHaveBeenCalledTimes(2); 67 | }); 68 | }); 69 | 70 | describe('registerAdCollectionSingleCall', () => { 71 | it('calls registerAd, requestBids, refresh', () => { 72 | 73 | const registerAdMock = jest.fn(); 74 | arcAds.registerAd = registerAdMock; 75 | 76 | const refreshMock = jest.fn(); 77 | window.googletag.pubads = jest.fn().mockReturnValue({refresh: refreshMock}); 78 | 79 | const requestBidsMock = jest.fn(); 80 | global.pbjs = {requestBids: requestBidsMock}; 81 | 82 | const adCollection = ['ad1', 'ad2']; 83 | 84 | arcAds.registerAdCollectionSingleCall(adCollection); 85 | 86 | 87 | expect(registerAdMock).toHaveBeenCalledTimes(2); 88 | expect(refreshMock).toHaveBeenCalledTimes(0); 89 | expect(requestBidsMock).toHaveBeenCalledTimes(1); 90 | }); 91 | }); 92 | 93 | describe('setAdsBlockGate', () => { 94 | 95 | it('sets blockArcAdsLoad to true if has window', () => { 96 | ArcAds.setAdsBlockGate(); 97 | expect(window.blockArcAdsLoad).toEqual(true); 98 | }); 99 | 100 | it('does nothing if no window', () => { 101 | const saveGetWindow = ArcAds.getWindow; 102 | const getWindowMock = jest.fn().mockReturnValue(undefined); 103 | ArcAds.getWindow = getWindowMock; 104 | 105 | ArcAds.setAdsBlockGate(); 106 | expect(getWindowMock()).toEqual(undefined); 107 | ArcAds.getWindow = saveGetWindow; 108 | }); 109 | }); 110 | 111 | describe('releaseAdsBlockGate', () => { 112 | 113 | it('sets blockArcAdsLoad to false if has window', () => { 114 | ArcAds.releaseAdsBlockGate(); 115 | expect(window.blockArcAdsLoad).toEqual(false); 116 | }); 117 | 118 | it('does nothing if no window', () => { 119 | const getWindowMock = jest.fn().mockReturnValue(undefined); 120 | ArcAds.getWindow = getWindowMock; 121 | 122 | ArcAds.releaseAdsBlockGate(); 123 | expect(getWindowMock()).toEqual(undefined); 124 | getWindowMock.mockRestore(); 125 | }); 126 | }); 127 | 128 | describe('sendSingleCallAds', () => { 129 | 130 | beforeAll(() => { 131 | registerAdCollectionSingleCallMock = jest.fn(); 132 | arcAds.registerAdCollectionSingleCall = registerAdCollectionSingleCallMock; 133 | }); 134 | 135 | it('if has nothing in adsList return', () => { 136 | arcAds.adsList = []; 137 | const result = arcAds.sendSingleCallAds(); 138 | expect(result).toEqual(false); 139 | }); 140 | 141 | it('if has adsList elems and pubads do SRA call', () => { 142 | arcAds.adsList = ['ad1', 'ad2']; 143 | window.googletag.pubadsReady = true; 144 | window.googletag.pubads =jest.fn().mockReturnValue({ 145 | disableInitialLoad: jest.fn(), 146 | enableSingleRequest: jest.fn(), 147 | enableAsyncRendering: jest.fn(), 148 | }); 149 | 150 | arcAds.sendSingleCallAds(); 151 | expect(registerAdCollectionSingleCallMock).toHaveBeenCalledTimes(1); 152 | }); 153 | }); 154 | 155 | describe('reserveAd', () => { 156 | it('sets block and adds ad to adsList', () => { 157 | const gateSetSpy = jest.spyOn(ArcAds, 'setAdsBlockGate'); 158 | arcAds.adsList = []; 159 | arcAds.reserveAd({example: true}); 160 | expect(gateSetSpy).toHaveBeenCalledTimes(1); 161 | expect(arcAds.adsList.length).toEqual(1); 162 | }); 163 | }); 164 | 165 | describe('setPageLeveTargeting', () => { 166 | it('sets block and adds ad to adsList', () => { 167 | const setTargetingMock = jest.fn(); 168 | 169 | window.googletag.pubads =jest.fn().mockReturnValue({ 170 | setTargeting: setTargetingMock, 171 | }); 172 | 173 | arcAds.setPageLeveTargeting('testKey', 'testValue'); 174 | expect(setTargetingMock).toHaveBeenCalledTimes(1); 175 | expect(setTargetingMock).toHaveBeenCalledWith('testKey', 'testValue'); 176 | }); 177 | }); 178 | 179 | }); 180 | -------------------------------------------------------------------------------- /src/services/sizemapping.js: -------------------------------------------------------------------------------- 1 | import { debounce } from '../util/debounce'; 2 | import { fetchBids } from './headerbidding'; 3 | import { refreshSlot } from './gpt'; 4 | import { sendLog } from '../util/log'; 5 | 6 | /** @desc An object containing all of the size map refresh event listeners and correlators for size mapping. **/ 7 | export const sizemapListeners = {}; 8 | 9 | /** @desc An object containing all of the screen resize event listeners for size mapping. **/ 10 | export const resizeListeners = {}; 11 | 12 | /** 13 | * @desc Prepares a set of dimensions and their corresponding breakpoints to create a sizemap which is readable by GPT. 14 | * @param {array} dimensions - An array containing all of the applicable sizes the advertisement can use. 15 | * @param {array} sizemap - An array containing all of the applicable breakpoints for the sizemapping. 16 | **/ 17 | export function prepareSizeMaps(dimensions, sizemap) { 18 | const mapping = []; 19 | const breakpoints = []; 20 | const correlators = []; 21 | const parsedSizemap = !sizemap.length ? null : sizemap; 22 | 23 | if (parsedSizemap && dimensions) { 24 | parsedSizemap.forEach((value, index) => { 25 | mapping.push([value, dimensions[index]]); 26 | 27 | // Filters duplicates from the mapping 28 | if (breakpoints.indexOf(value[0]) === -1) { 29 | breakpoints.push(value[0]); 30 | correlators.push(false); 31 | } 32 | }); 33 | } 34 | 35 | breakpoints.sort((a, b) => { return a - b; }); 36 | 37 | return { mapping, breakpoints, correlators }; 38 | } 39 | 40 | /** 41 | * @desc Determines which set of ad sizes are about to display based on the users current screen size. 42 | * @param {array} sizeMappings - An array containing the advertisements GPT readable size mapping. 43 | * @return {array} - Returns an array containing the ad sizes which relate to the users current window width. 44 | **/ 45 | export function parseSizeMappings(sizeMappings) { 46 | try { 47 | const width = window.innerWidth || 48 | document.documentElement.clientWidth || 49 | document.body.clientWidth; 50 | 51 | const height = window.innerHeight || 52 | document.documentElement.clientHeight || 53 | document.body.clientHeight; 54 | 55 | const sd = [width, height]; 56 | 57 | /* Filters mappings that are valid by confirming that the current screen dimensions 58 | are both greater than or equal to the breakpoint [x, y] minimums specified in the first position in the mapping. 59 | Returns the leftmost mapping's sizes or an empty array. */ 60 | const validMappings = sizeMappings.filter((mapping) => { 61 | return mapping[0][0] <= sd[0] && mapping[0][1] <= sd[1]; 62 | }); 63 | 64 | let result = validMappings.length > 0 ? validMappings[0][1] : []; 65 | 66 | if (result.length > 0 && result[0].constructor !== Array) { 67 | // Wraps the 1D array in another set of brackets to make it 2D 68 | result = [result]; 69 | } 70 | 71 | return result; 72 | } catch (e) { 73 | sendLog('parseSizeMappings()', 'invalid size mapping', null); 74 | // Fallback to last size mapping supplied if there's an invalid mapping provided 75 | return sizeMappings[sizeMappings.length - 1][1]; 76 | } 77 | } 78 | 79 | /** 80 | * @desc Resize event that checks if a user has resized past a breakpoint included in the advertisements sizemap. If it has the GPT 81 | * refresh method is called so the service can fetch a more apropriately sized creative. 82 | * @param {object} params - An object containing all of the advertisement configuration settings such as slot name, id, and position. 83 | **/ 84 | export function runResizeEvents(params) { 85 | let lastBreakpoint; 86 | let initialLoad = false; 87 | 88 | if (params.breakpoints) { 89 | /** 90 | * Initially set lastBreakpoint to be the largest breakpoint 91 | * that's smaller than the current window width 92 | **/ 93 | const initialWidth = window.innerWidth; 94 | lastBreakpoint = params.breakpoints.filter(bp => bp < initialWidth).pop() || params.breakpoints[0]; 95 | } 96 | 97 | return () => { 98 | const { 99 | ad, 100 | breakpoints, 101 | id, 102 | bidding, 103 | mapping, 104 | slotName, 105 | wrapper, 106 | prerender } = params; 107 | 108 | const width = window.innerWidth; 109 | let breakpoint; 110 | let nextBreakpoint; 111 | 112 | for (let i = 0; i < breakpoints.length; i++) { 113 | breakpoint = breakpoints[i]; 114 | nextBreakpoint = breakpoints[i + 1]; 115 | 116 | if (lastBreakpoint !== breakpoint && ((width > breakpoint && (width < nextBreakpoint || !nextBreakpoint)) || (width === breakpoint && !initialLoad))) { 117 | lastBreakpoint = breakpoint; 118 | initialLoad = true; 119 | 120 | // Fetches a set of dimensions for the ad which is about to display. 121 | const parsedSizeMapping = parseSizeMappings(mapping); 122 | 123 | const adInfo = { 124 | adUnit: ad, 125 | adSlot: slotName, 126 | adDimensions: parsedSizeMapping, 127 | adId: id 128 | }; 129 | 130 | // If it's included in a header-bidding service we re-fetch bids for the given slot, otherwise it refreshes as normal. 131 | if ((bidding.prebid && bidding.prebid.enabled) || (bidding.amazon && bidding.amazon.enabled)) { 132 | fetchBids({ 133 | ad, 134 | id, 135 | slotName, 136 | dimensions: parsedSizeMapping, 137 | bidding, 138 | wrapper, 139 | prerender, 140 | correlator: sizemapListeners[id].correlators[i], 141 | breakpoints 142 | }); 143 | } else { 144 | refreshSlot({ 145 | ad, 146 | correlator: sizemapListeners[id].correlators[i], 147 | prerender, 148 | info: adInfo 149 | }); 150 | } 151 | } 152 | 153 | sizemapListeners[id].correlators[i] = true; 154 | } 155 | }; 156 | } 157 | 158 | /** 159 | * @desc Assigns an event listener for a size mapped ad which detects when the screen resizes past a breakpoint in the sizemap. 160 | * Also stores the event listener in an object sorted by the advertisement id so it can be unbound later if needed. 161 | * @param {object} params - An object containing all of the advertisement configuration settings such as slot name, id, and position. 162 | **/ 163 | export function setResizeListener(params) { 164 | const { id, correlators } = params; 165 | 166 | resizeListeners[id] = debounce(runResizeEvents(params), 250); 167 | window.addEventListener('resize', resizeListeners[id]); 168 | 169 | // Adds the listener to an object with the id as the key so we can unbind it later. 170 | sizemapListeners[id] = { listener: resizeListeners[id], correlators }; 171 | } 172 | 173 | -------------------------------------------------------------------------------- /src/__tests__/gpt.test.js: -------------------------------------------------------------------------------- 1 | import { ArcAds } from '../index'; 2 | import * as gpt from '../services/gpt'; 3 | import * as headerbidding from '../services/headerbidding'; 4 | import * as sizemap from '../services/sizemapping'; 5 | import * as queryUtil from '../util/query'; 6 | 7 | describe('arcads', () => { 8 | const methods = { 9 | queueGoogletagCommand: jest.spyOn(gpt, 'queueGoogletagCommand'), 10 | setTargeting: jest.spyOn(gpt, 'setTargeting'), 11 | refreshSlot: jest.spyOn(gpt, 'refreshSlot'), 12 | fetchBids: jest.spyOn(headerbidding, 'fetchBids'), 13 | prepareSizeMaps: jest.spyOn(sizemap, 'prepareSizeMaps'), 14 | setResizeListener: jest.spyOn(sizemap, 'setResizeListener') 15 | }; 16 | 17 | const arcAds = new ArcAds({ 18 | dfp: { 19 | id: '123' 20 | }, 21 | bidding: { 22 | amazon: { 23 | enabled: true, 24 | id: '123' 25 | }, 26 | prebid: { 27 | enabled: true 28 | } 29 | } 30 | }); 31 | 32 | global.googletag = { 33 | defineSlot: () => global.googletag, 34 | defineOutOfPageSlot: () => global.googletag, 35 | defineSizeMapping: () => global.googletag, 36 | addService: () => global.googletag, 37 | setTargeting: () => global.googletag, 38 | pubads: () => global.googletag, 39 | refresh: () => global.googletag, 40 | cmd: [] 41 | }; 42 | 43 | describe('google publisher tag', () => { 44 | it('should push a function to the gpt queue', () => { 45 | const fn = () => 'montezuma'; 46 | gpt.queueGoogletagCommand(fn); 47 | 48 | expect(global.googletag.cmd.length).toBe(1); 49 | expect(global.googletag.cmd[0]).toBe(fn); 50 | }); 51 | 52 | it('should display a regular advertisement', () => { 53 | arcAds.displayAd({ 54 | id: 'div-id-123', 55 | slotName: 'hp/hp-1', 56 | adType: 'cube', 57 | dimensions: [[300, 250], [300, 600]], 58 | display: 'all', 59 | targeting: { 60 | section: 'weather' 61 | } 62 | }); 63 | 64 | expect(methods.setTargeting.mock.calls.length).toBe(1); 65 | expect(methods.refreshSlot.mock.calls.length).toBe(1); 66 | }); 67 | 68 | it('should display a header bidding enabled advertisement', () => { 69 | arcAds.displayAd({ 70 | id: 'div-id-123', 71 | slotName: 'hp/hp-1', 72 | adType: 'cube', 73 | dimensions: [[300, 250], [300, 600]], 74 | display: 'all', 75 | targeting: { 76 | section: 'weather' 77 | }, 78 | bidding: { 79 | amazon: { 80 | enabled: true 81 | } 82 | } 83 | }); 84 | 85 | expect(methods.setTargeting.mock.calls.length).toBe(2); 86 | expect(methods.fetchBids.mock.calls.length).toBe(1); 87 | }); 88 | 89 | it('should display a size mapped advertisement', () => { 90 | arcAds.displayAd({ 91 | id: 'div-id-123', 92 | slotName: 'hp/hp-1', 93 | adType: 'cube', 94 | dimensions: [ [[970, 250], [970, 90], [728, 90]], [[728, 90]], [[320, 100], [320, 50]] ], 95 | targeting: { 96 | section: 'weather' 97 | }, 98 | sizemap: { 99 | breakpoints: [ [1280, 0], [800, 0], [0, 0] ], 100 | refresh: 'true' 101 | } 102 | }); 103 | 104 | expect(methods.prepareSizeMaps.mock.calls.length).toBe(1); 105 | expect(methods.setResizeListener.mock.calls.length).toBe(1); 106 | }); 107 | }); 108 | 109 | it('if has prerender that resolves call refresh', () => { 110 | window.googletag.pubadsReady = true; 111 | const prerenderFnc = jest.fn(); 112 | gpt.refreshSlot({ad:{name:"ad"}, correlator:false, prerender:prerenderFnc, info:{}}); 113 | expect(prerenderFnc).toHaveBeenCalledTimes(1); 114 | }); 115 | 116 | it('if blockarcAds load is set do not call pubads refresh', () => { 117 | window.googletag.pubadsReady = true; 118 | window.blockArcAdsLoad = true; 119 | const prerenderFnc = jest.fn(); 120 | 121 | const refreshMock = jest.fn(); 122 | global.googletag.pubads = jest.fn().mockReturnValue({ 123 | refresh: refreshMock, 124 | }); 125 | gpt.refreshSlot({ad:{name:"ad"}, correlator:false, prerender:prerenderFnc, info:{}}); 126 | expect(refreshMock).toHaveBeenCalledTimes(0); 127 | }); 128 | 129 | describe('setTargeting', () => { 130 | it('if options has key and value call ad SetTargeting', () => { 131 | const setTargetingMock = jest.fn(); 132 | const ad = {setTargeting: setTargetingMock}; 133 | gpt.setTargeting(ad, {testKey:"testValue"}); 134 | expect(setTargetingMock).toHaveBeenCalledTimes(1); 135 | expect(setTargetingMock).toHaveBeenCalledWith("testKey","testValue" ); 136 | }); 137 | 138 | it('if options has NOT key and value call ad SetTargeting', () => { 139 | const setTargetingMock = jest.fn(); 140 | const ad = {setTargeting: setTargetingMock}; 141 | gpt.setTargeting(ad, {testKey:null}); 142 | expect(setTargetingMock).toHaveBeenCalledTimes(0); 143 | }); 144 | }); 145 | 146 | describe('dfpSettings', () => { 147 | 148 | const disableInitialLoadMock = jest.fn(); 149 | const enableSingleRequestMock = jest.fn(); 150 | const enableAsyncRenderingMock = jest.fn(); 151 | const enableServicesMock = jest.fn(); 152 | const collapseEmptyDivsMock = jest.fn(); 153 | const addEventListenerMock = jest.fn(); 154 | 155 | beforeAll(() => { 156 | global.googletag.pubads = jest.fn().mockReturnValue({ 157 | disableInitialLoad: disableInitialLoadMock, 158 | enableSingleRequest : enableSingleRequestMock, 159 | enableAsyncRendering: enableAsyncRenderingMock, 160 | collapseEmptyDivs: collapseEmptyDivsMock, 161 | addEventListener: addEventListenerMock, 162 | }); 163 | 164 | global.googletag.enableServices = enableServicesMock; 165 | }); 166 | 167 | afterEach(() => { 168 | jest.clearAllMocks(); 169 | }); 170 | 171 | it('call non-logic dependent pubads setup functions', () => { 172 | gpt.dfpSettings(); 173 | expect(disableInitialLoadMock).toHaveBeenCalledTimes(1); 174 | expect(enableSingleRequestMock).toHaveBeenCalledTimes(1); 175 | expect(enableAsyncRenderingMock).toHaveBeenCalledTimes(1); 176 | expect(enableServicesMock).toHaveBeenCalledTimes(1); 177 | expect(enableAsyncRenderingMock).toHaveBeenCalledTimes(1); 178 | expect(collapseEmptyDivsMock).toHaveBeenCalledTimes(0); 179 | expect(addEventListenerMock).toHaveBeenCalledTimes(0); 180 | }); 181 | 182 | it( 'if handleSlotRenderEnded function calls addEventListener', () => { 183 | const handleMock = jest.fn(); 184 | gpt.dfpSettings(handleMock); 185 | expect(disableInitialLoadMock).toHaveBeenCalledTimes(1); 186 | expect(enableSingleRequestMock).toHaveBeenCalledTimes(1); 187 | expect(enableAsyncRenderingMock).toHaveBeenCalledTimes(1); 188 | expect(enableServicesMock).toHaveBeenCalledTimes(1); 189 | expect(enableAsyncRenderingMock).toHaveBeenCalledTimes(1); 190 | expect(addEventListenerMock).toHaveBeenCalledTimes(1); 191 | }); 192 | }); 193 | 194 | describe('determineSlotName', () => { 195 | it('return slotname based on dfpCode and slotname args', () => { 196 | const result = gpt.determineSlotName('dfpCode',"testSlotname"); 197 | expect(result).toEqual('/dfpCode/testSlotname'); 198 | }); 199 | 200 | it('ifAdsSlot override then use that value for slotaName', () => { 201 | jest.spyOn(queryUtil, 'expandQueryString').mockReturnValue('overrideSlotname'); 202 | const result = gpt.determineSlotName('dfpCode',"testSlotname"); 203 | expect(result).toEqual('/dfpCode/overrideSlotname'); 204 | }); 205 | }); 206 | }); 207 | -------------------------------------------------------------------------------- /src/__tests__/displayAd.test.js: -------------------------------------------------------------------------------- 1 | import { ArcAds } from '../index'; 2 | import * as gptService from '../services/gpt.js'; 3 | import * as sizemappingService from '../services/sizemapping.js' 4 | import * as headerBidding from '../services/headerbidding.js'; 5 | 6 | describe('displayAd ', () => { 7 | 8 | beforeEach(() => { 9 | jest.clearAllMocks(); 10 | }); 11 | 12 | const defineOutOfPageSlotMock = jest.fn(); 13 | const defineSlotMock = jest.fn(); 14 | 15 | window.googletag= { 16 | defineOutOfPageSlot: defineOutOfPageSlotMock, 17 | defineSlot: defineSlotMock, 18 | pubads: jest.fn(), 19 | }; 20 | const refreshSlotSpy = jest.spyOn(gptService, 'refreshSlot'); 21 | const setResizeListenerSpy = jest.spyOn(sizemappingService, 'setResizeListener'); 22 | const fetchBidsSpy = jest.spyOn(headerBidding, 'fetchBids'); 23 | 24 | 25 | const arcAds = new ArcAds({ 26 | dfp: { 27 | id: '123' 28 | }, 29 | bidding: { 30 | amazon: { 31 | enabled: true, 32 | id: '123' 33 | }, 34 | prebid: { 35 | enabled: true 36 | } 37 | } 38 | }); 39 | 40 | it('if does not have dimensions should call defineOutOfPageSlot', () => { 41 | const adParams = { 42 | id: "testID", 43 | slotName: 'testSlotname', 44 | dimensions: null, 45 | targeting: null, 46 | sizemap: {breakpoints:[0, 50]}, 47 | bidding: false, 48 | prerender: null, 49 | }; 50 | window.blockArcAdsPrebid = false; 51 | const result = arcAds.displayAd(adParams); 52 | 53 | expect(result).toEqual(undefined); 54 | expect(defineOutOfPageSlotMock).toHaveBeenCalledTimes(1); 55 | expect(defineOutOfPageSlotMock).toHaveBeenCalledWith("/123/testSlotname", "testID"); 56 | expect(defineSlotMock).toHaveBeenCalledTimes(0); 57 | 58 | expect(refreshSlotSpy).toHaveBeenCalledTimes(1); 59 | const expectedRefreshSLotParams = {"ad": undefined, "info": {"adDimensions": null, "adId": "testID", "adSlot": "/123/testSlotname", "adUnit": undefined}, "prerender": null}; 60 | expect(refreshSlotSpy).toHaveBeenCalledWith(expectedRefreshSLotParams); 61 | 62 | }); 63 | 64 | it('if no ad object return false', () => { 65 | const adParams = { 66 | id: "testID", 67 | slotName: 'testSlotname', 68 | dimensions: [100,40], 69 | targeting: null, 70 | sizemap: {breakpoints:[0, 50]}, 71 | bidding: false, 72 | prerender: null, 73 | }; 74 | 75 | defineOutOfPageSlotMock.mockReturnValue(null); 76 | window.blockArcAdsPrebid = false; 77 | const result = arcAds.displayAd(adParams); 78 | 79 | expect(result).toEqual(false); 80 | expect(defineOutOfPageSlotMock).toHaveBeenCalledTimes(0); 81 | expect(defineSlotMock).toHaveBeenCalledTimes(1); 82 | expect(defineSlotMock).toHaveBeenCalledWith( "/123/testSlotname", [100, 40], "testID"); 83 | 84 | expect(refreshSlotSpy).toHaveBeenCalledTimes(0); 85 | }); 86 | 87 | it('if sizemap.refresh call resize listener', () => { 88 | const adParams = { 89 | id: "testID", 90 | slotName: 'testSlotname', 91 | dimensions: [100,40], 92 | targeting: null, 93 | sizemap: {breakpoints:[0, 50], refresh: true}, 94 | bidding: false, 95 | prerender: null, 96 | }; 97 | 98 | window.blockArcAdsPrebid = true; 99 | 100 | const defineSizeMappingMock = jest.fn(); 101 | defineSlotMock.mockReturnValue({ 102 | defineSizeMapping: defineSizeMappingMock, 103 | addService: jest.fn() 104 | }); 105 | const result = arcAds.displayAd(adParams); 106 | 107 | expect(result).toEqual(undefined); 108 | expect(defineOutOfPageSlotMock).toHaveBeenCalledTimes(0); 109 | 110 | expect(defineSlotMock).toHaveBeenCalledTimes(1); 111 | expect(defineSlotMock).toHaveBeenCalledWith( "/123/testSlotname", [100, 40], "testID"); 112 | 113 | expect(refreshSlotSpy).toHaveBeenCalledTimes(0); 114 | 115 | expect(defineSizeMappingMock).toHaveBeenCalledTimes(1); 116 | expect(defineSizeMappingMock).toHaveBeenCalledWith([[0, 100], [50, 40]]); 117 | 118 | expect(setResizeListenerSpy).toHaveBeenCalledTimes(1); 119 | 120 | expect(setResizeListenerSpy.mock.calls[0][0]).toEqual( 121 | expect.objectContaining({ 122 | "bidding": false, 123 | "breakpoints": [undefined], 124 | "correlators": [false], 125 | "id": "testID", 126 | "mapping": [[0, 100], [50, 40]], 127 | "prerender": null, 128 | "slotName": "/123/testSlotname", 129 | "wrapper": {"amazon": {"enabled": true, "id": "123"}, "prebid": {"enabled": true}}} 130 | ) 131 | ); 132 | }); 133 | 134 | it('if no sizemap.refresh, do NOT call resize listener', () => { 135 | const adParams = { 136 | id: "testID", 137 | slotName: 'testSlotname', 138 | dimensions: [100,40], 139 | targeting: null, 140 | sizemap: {breakpoints:[0, 50], refresh: false}, 141 | bidding: false, 142 | prerender: null, 143 | }; 144 | 145 | window.blockArcAdsPrebid = true; 146 | 147 | const defineSizeMappingMock = jest.fn(); 148 | defineSlotMock.mockReturnValue({ 149 | defineSizeMapping: defineSizeMappingMock, 150 | addService: jest.fn() 151 | }); 152 | arcAds.displayAd(adParams); 153 | expect(setResizeListenerSpy).toHaveBeenCalledTimes(0); 154 | }); 155 | 156 | it('if has adsList and ad push ad to adsList', () => { 157 | const adParams = { 158 | id: "testID", 159 | slotName: 'testSlotname', 160 | dimensions: [100,40], 161 | targeting: null, 162 | sizemap: {breakpoints:[0, 50], refresh: false}, 163 | bidding: false, 164 | prerender: null, 165 | }; 166 | 167 | window.blockArcAdsPrebid = true; 168 | window.adsList = []; 169 | 170 | const defineSizeMappingMock = jest.fn(); 171 | defineSlotMock.mockReturnValue({ 172 | defineSizeMapping: defineSizeMappingMock, 173 | addService: jest.fn() 174 | }); 175 | arcAds.displayAd(adParams); 176 | expect(window.adsList.length).toEqual(1); 177 | }); 178 | 179 | it('if has bidding.prebid.enabled call fetchBids', () => { 180 | const adParams = { 181 | id: "testID", 182 | slotName: 'testSlotname', 183 | dimensions: [100,40], 184 | targeting: null, 185 | sizemap: {breakpoints:[0, 50], refresh: false}, 186 | bidding: {prebid: {enabled: true}}, 187 | prerender: null, 188 | }; 189 | 190 | window.blockArcAdsPrebid = false; 191 | arcAds.displayAd(adParams); 192 | 193 | expect(refreshSlotSpy).toHaveBeenCalledTimes(0); 194 | expect(fetchBidsSpy).toHaveBeenCalledTimes(1); 195 | }); 196 | 197 | it('handles non-null dimnsions length 0 case', () => { 198 | const adParams = { 199 | id: "testID", 200 | slotName: 'testSlotname', 201 | dimensions: [], 202 | targeting: null, 203 | sizemap: {breakpoints:[0, 50], refresh: false}, 204 | bidding: {prebid: {enabled: true}}, 205 | prerender: null, 206 | }; 207 | 208 | const defineSizeMappingMock = jest.fn(); 209 | defineSlotMock.mockReturnValue({ 210 | defineSizeMapping: defineSizeMappingMock, 211 | addService: jest.fn() 212 | }); 213 | 214 | const result = arcAds.displayAd(adParams); 215 | 216 | expect(result).toEqual(undefined); 217 | expect(defineOutOfPageSlotMock).toHaveBeenCalledTimes(0); 218 | 219 | expect(defineSlotMock).toHaveBeenCalledTimes(1); 220 | expect(defineSlotMock).toHaveBeenCalledWith( "/123/testSlotname", null, "testID"); 221 | 222 | expect(refreshSlotSpy).toHaveBeenCalledTimes(0); 223 | 224 | expect(defineSizeMappingMock).toHaveBeenCalledTimes(1); 225 | expect(defineSizeMappingMock).toHaveBeenCalledWith([]); 226 | 227 | expect(setResizeListenerSpy).toHaveBeenCalledTimes(0); 228 | 229 | }); 230 | 231 | }); -------------------------------------------------------------------------------- /src/__tests__/registerAds.test.js: -------------------------------------------------------------------------------- 1 | import { ArcAds } from '../index'; 2 | import * as gptService from '../services/gpt.js'; 3 | import * as prebidService from '../services/prebid.js'; 4 | import * as mobileDetection from '../util/mobile.js'; 5 | 6 | 7 | describe('registerAds dimensions branches', () => { 8 | 9 | global.pbjs = { 10 | que: [], 11 | addAdUnits: () => jest.fn().mockName('addAdUnits'), 12 | requestBids: () => jest.fn().mockName('requestBids'), 13 | setConfig: () => jest.fn().mockName('setConfig'), 14 | setTargetingForGPTAsync: jest.fn().mockName('setTargetingForGPTAsync'), 15 | }; 16 | 17 | const setConfigSpy = jest.spyOn(pbjs, 'setConfig'); 18 | 19 | //queueGoogletagCommand mock 20 | jest.spyOn(gptService, 'queueGoogletagCommand'); 21 | //const queuePrebidCommandMock = jest.fn(); 22 | const queuePrebidCommandBindMock = jest.fn(); 23 | prebidService.queuePrebidCommand = queuePrebidCommandBindMock; 24 | prebidService.queuePrebidCommand.bind = queuePrebidCommandBindMock; 25 | 26 | // prebidService.queuePrebidCommand.bind = queuePrebidCommandBindMock; 27 | const addUnitMock = jest.fn(); 28 | prebidService.addUnit = addUnitMock; 29 | 30 | const arcAds = new ArcAds({ 31 | dfp: { 32 | id: '123' 33 | }, 34 | bidding: { 35 | amazon: { 36 | enabled: true, 37 | id: '123' 38 | }, 39 | prebid: { 40 | enabled: true 41 | } 42 | } 43 | }); 44 | 45 | //displayAd mock 46 | const displayAdMock = jest.fn(); 47 | const displayAdBindMock = jest.fn().mockReturnValue(jest.fn()); 48 | arcAds.displayAd = displayAdMock; 49 | arcAds.displayAd.bind = displayAdBindMock; 50 | 51 | beforeEach(() => { 52 | jest.clearAllMocks(); 53 | }); 54 | 55 | it('should should call prebid setConfig if has bidding configs and prebid lib is present', () => { 56 | //ArcAds obj 57 | const adParams = { 58 | id: "testID", 59 | slotname: "testSlotname", 60 | dimensions: [[300, 50], [300, 250]] 61 | } 62 | 63 | arcAds.registerAd(adParams); 64 | 65 | expect(gptService.queueGoogletagCommand).toHaveBeenCalledTimes(1); 66 | expect(prebidService.queuePrebidCommand).toHaveBeenCalledTimes(0); 67 | 68 | }); 69 | 70 | it('should add single level dimensions appropriately', () => { 71 | const adParams = { 72 | id: "testID", 73 | slotname: "testSlotname", 74 | dimensions: [300, 50], 75 | } 76 | arcAds.registerAd(adParams); 77 | 78 | expect(arcAds.displayAd.bind).toHaveBeenCalledTimes(1); 79 | 80 | const expectedArg1 = {"adsList": [], 81 | "collapseEmptyDivs": undefined, 82 | "dfpId": "123", 83 | "displayAd": displayAdMock, 84 | "positions": [], 85 | "wrapper": {"amazon": {"enabled": true, "id": "123"}, 86 | "prebid": {"enabled": true}} 87 | }; 88 | const expectedArg2 = {"dimensions": [300, 50], "id": "testID", "slotname": "testSlotname"}; 89 | 90 | expect(displayAdBindMock).toHaveBeenCalledWith(expectedArg1, expectedArg2); 91 | 92 | }); 93 | 94 | it('should add two level dimensions appropriately', () => { 95 | 96 | const adParams = { 97 | id: "testID", 98 | slotname: "testSlotname", 99 | dimensions: [[300, 50], [250, 100]], 100 | } 101 | 102 | arcAds.registerAd(adParams); 103 | 104 | expect(displayAdBindMock).toHaveBeenCalledTimes(1); 105 | 106 | const expectedArg1 = {"adsList": [], 107 | "collapseEmptyDivs": undefined, 108 | "dfpId": "123", 109 | "displayAd": displayAdMock, 110 | "positions": [], 111 | "wrapper": {"amazon": {"enabled": true, "id": "123"}, 112 | "prebid": {"enabled": true}} 113 | }; 114 | const expectedArg2 = {"dimensions": [[300, 50], [250, 100]], "id": "testID", "slotname": "testSlotname"}; 115 | expect(displayAdBindMock).toHaveBeenCalledWith(expectedArg1, expectedArg2); 116 | 117 | }); 118 | 119 | it('should add no dimensions appropriately', () => { 120 | const adParams = { 121 | id: "testID", 122 | slotname: "testSlotname", 123 | dimensions: undefined, 124 | } 125 | arcAds.registerAd(adParams); 126 | 127 | expect(displayAdBindMock).toHaveBeenCalledTimes(1); 128 | 129 | const expectedArg1 = {"adsList": [], 130 | "collapseEmptyDivs": undefined, 131 | "dfpId": "123", 132 | "displayAd": displayAdMock, 133 | "positions": [], 134 | "wrapper": {"amazon": {"enabled": true, "id": "123"}, 135 | "prebid": {"enabled": true}}}; 136 | const expectedArg2 = {"dimensions": undefined, "id": "testID", "slotname": "testSlotname"}; 137 | expect(displayAdBindMock).toHaveBeenCalledWith(expectedArg1, expectedArg2); 138 | }); 139 | 140 | it('should add non 1 or 2 level dimensions appropriately', () => { 141 | const adParams = { 142 | id: "testID", 143 | slotname: "testSlotname", 144 | dimensions: [[[100,50]]], 145 | targeting:{}, 146 | adType: true, 147 | display: 'mobile', 148 | bidding:{prebid:{enabled: true, bids:['bid1']}} 149 | } 150 | 151 | const mobileAny = jest.fn().mockReturnValue(true); 152 | global.isMobile = {any: mobileAny}; 153 | 154 | arcAds.registerAd(adParams); 155 | 156 | expect(setConfigSpy).toHaveBeenCalledTimes(1); 157 | expect(setConfigSpy.mock.calls[0][0]).toEqual( 158 | expect.objectContaining({ 159 | "userSync": { 160 | "filterSettings": { 161 | "iframe": { 162 | "bidders": ["openx"], "filter": "include"} 163 | }, 164 | "iframeEnabled": true 165 | } 166 | }) 167 | ); 168 | 169 | expect(displayAdBindMock).toHaveBeenCalledTimes(1); 170 | const expectedArg2 = { 171 | "adType": true, 172 | "bidding": {"prebid": {"enabled": true, "bids": ["bid1"]}}, 173 | "dimensions": [[[100, 50]]], 174 | "display": "mobile", 175 | "id": "testID", 176 | "slotname": "testSlotname", 177 | "targeting": {"position": 1} 178 | }; 179 | expect(displayAdBindMock.mock.calls[0][1]).toEqual( expectedArg2); 180 | }); 181 | 182 | it('prebid should not be called', () => { 183 | const adParams = { 184 | id: "testID", 185 | slotname: "testSlotname", 186 | dimensions: [[[100,50]]], 187 | targeting:{}, 188 | adType: true, 189 | display: 'mobile', 190 | bidding:{prebid:{enabled: false, bids:['bid1']}} 191 | } 192 | 193 | arcAds.registerAd(adParams); 194 | expect(addUnitMock).toHaveBeenCalledTimes(0); 195 | }); 196 | 197 | it('wrapper has useSlotForAdUnit for caclulating prebid code', () => { 198 | arcAds.wrapper = { 199 | amazon: { 200 | enabled: true, 201 | id: '123' 202 | }, 203 | prebid: { 204 | enabled: true, 205 | useSlotForAdUnit: true, 206 | } 207 | }; 208 | const adParams = { 209 | id: "testID", 210 | slotname: "testSlotname", 211 | dimensions: [[[100,50]]], 212 | targeting:{}, 213 | adType: true, 214 | display: 'mobile', 215 | bidding:{prebid:{enabled: true, bids:['bid1']}} 216 | } 217 | 218 | const mobileAny = jest.fn().mockReturnValue(true); 219 | global.isMobile = {any: mobileAny}; 220 | 221 | arcAds.registerAd(adParams); 222 | 223 | expect(addUnitMock).toHaveBeenCalledTimes(1); 224 | expect(addUnitMock.mock.calls[0][0]).toEqual("/123/undefined"); 225 | 226 | expect(queuePrebidCommandBindMock).toHaveBeenCalledTimes(1); 227 | }); 228 | 229 | it('handles no display case', () => { 230 | arcAds.wrapper = { 231 | amazon: { 232 | enabled: true, 233 | id: '123' 234 | }, 235 | prebid: { 236 | enabled: true, 237 | useSlotForAdUnit: true, 238 | } 239 | }; 240 | const adParams = { 241 | id: "testID", 242 | slotname: "testSlotname", 243 | dimensions: [[[100,50]]], 244 | targeting:{}, 245 | adType: true, 246 | display: 'other', 247 | bidding:{prebid:{bids:['bid1']}} 248 | } 249 | 250 | const mobileAny = jest.fn().mockReturnValue(true); 251 | global.isMobile = {any: mobileAny}; 252 | 253 | arcAds.registerAd(adParams); 254 | 255 | expect(queuePrebidCommandBindMock).toHaveBeenCalledTimes(0); 256 | expect(displayAdBindMock).toHaveBeenCalledTimes(0); 257 | }); 258 | 259 | it('handles iframeBidders case', () => { 260 | arcAds.wrapper = { 261 | amazon: { 262 | enabled: true, 263 | id: '123' 264 | }, 265 | prebid: { 266 | enabled: true, 267 | useSlotForAdUnit: true, 268 | } 269 | }; 270 | const adParams = { 271 | id: "testID", 272 | slotname: "testSlotname", 273 | dimensions: [[[100,50]]], 274 | targeting:{}, 275 | adType: true, 276 | display: 'all', 277 | bidding:{prebid:{bids:['bid1']}}, 278 | iframeBidders:[], 279 | } 280 | 281 | const mobileAny = jest.fn().mockReturnValue(true); 282 | global.isMobile = {any: mobileAny}; 283 | 284 | arcAds.registerAd(adParams); 285 | 286 | expect(setConfigSpy).toHaveBeenCalledTimes(0); 287 | }); 288 | 289 | it('if no processDisplayAd do not call queueGoogletagCommand' , () => { 290 | arcAds.wrapper = { 291 | amazon: { 292 | enabled: true, 293 | id: '123' 294 | }, 295 | prebid: { 296 | enabled: true, 297 | useSlotForAdUnit: true, 298 | } 299 | }; 300 | const adParams = { 301 | id: "testID", 302 | slotname: "testSlotname", 303 | dimensions: [[[100,50]]], 304 | targeting:{}, 305 | adType: true, 306 | display: 'all', 307 | bidding:{prebid:{bids:['bid1']}}, 308 | iframeBidders:[], 309 | } 310 | arcAds.displayAd.bind = jest.fn().mockReturnValue(null); 311 | 312 | arcAds.registerAd(adParams); 313 | 314 | expect(gptService.queueGoogletagCommand).toHaveBeenCalledTimes(0); 315 | }); 316 | 317 | it('if try error write console error' , () => { 318 | arcAds.wrapper = { 319 | amazon: { 320 | enabled: true, 321 | id: '123' 322 | }, 323 | prebid: { 324 | enabled: true, 325 | useSlotForAdUnit: true, 326 | } 327 | }; 328 | const adParams = { 329 | id: "testID", 330 | slotname: "testSlotname", 331 | dimensions: [[[100,50]]], 332 | targeting:{}, 333 | adType: true, 334 | display: 'all', 335 | bidding:{prebid:{bids:['bid1']}}, 336 | iframeBidders:[], 337 | } 338 | arcAds.displayAd.bind.mockImplementation(() => { 339 | throw new Error('test error msg'); 340 | }); 341 | 342 | const errorMock = jest.fn(); 343 | console.error = errorMock; 344 | 345 | arcAds.registerAd(adParams); 346 | 347 | expect(errorMock).toHaveBeenCalledTimes(1); 348 | expect(errorMock.mock.calls[0][0]).toEqual('ads error'); 349 | }); 350 | 351 | }); -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import { MobileDetection } from './util/mobile'; 2 | import { sendLog } from './util/log'; 3 | import { fetchBids, initializeBiddingServices } from './services/headerbidding'; 4 | import { initializeGPT, queueGoogletagCommand, refreshSlot, dfpSettings, setTargeting, determineSlotName } from './services/gpt'; 5 | import { queuePrebidCommand, addUnit } from './services/prebid'; 6 | import { prepareSizeMaps, setResizeListener } from './services/sizemapping'; 7 | 8 | function getArrayDepth(array) { 9 | return Array.isArray(array) 10 | ? 1 + Math.max(...array.map(child => getArrayDepth(child))) 11 | : 0; 12 | } 13 | 14 | /** @desc Displays an advertisement from Google DFP with optional support for Prebid.js and Amazon TAM/A9. **/ 15 | export class ArcAds { 16 | constructor(options, handleSlotRendered = null) { 17 | this.dfpId = options.dfp.id || ''; 18 | this.wrapper = options.bidding || {}; 19 | this.positions = []; 20 | this.collapseEmptyDivs = options.dfp.collapseEmptyDivs; 21 | this.adsList = []; 22 | window.isMobile = MobileDetection; 23 | 24 | if (this.dfpId === '') { 25 | console.warn( 26 | 'ArcAds: DFP id is missing from the arcads initialization script.', 27 | '\n', 28 | 'Documentation: https://github.com/washingtonpost/arcads#getting-started' 29 | ); 30 | sendLog('constructor()', 'The DFP id missing from the arcads initialization script. ArcAds cannot proceed.', null); 31 | } else { 32 | initializeGPT(); 33 | queueGoogletagCommand(dfpSettings.bind(this, handleSlotRendered)); 34 | initializeBiddingServices(this.wrapper); 35 | } 36 | } 37 | 38 | /** 39 | * @desc Registers an advertisement in the service. 40 | * @param {object} params - An object containing all of the advertisement configuration settings such as slot name, id, and position. 41 | **/ 42 | registerAd(params) { 43 | const { id, slotName, dimensions, adType = false, targeting = {}, display = 'all', bidding = false, iframeBidders = ['openx'], others = {} } = params; 44 | const flatDimensions = []; 45 | let processDisplayAd = false; 46 | const dimensionsDepth = getArrayDepth(dimensions); 47 | 48 | if (dimensions && typeof dimensions !== 'undefined' && dimensionsDepth === 1) { 49 | flatDimensions.push(...dimensions); 50 | } else if (dimensions && typeof dimensions !== 'undefined' && dimensions.length > 0 && dimensionsDepth === 2) { 51 | flatDimensions.push(...dimensions); 52 | } else if (dimensions) { 53 | dimensions.forEach((set) => { 54 | flatDimensions.push(...set); 55 | }); 56 | } 57 | 58 | try { 59 | /* If positional targeting doesn't exist it gets assigned a numeric value 60 | based on the order and type of the advertisement. This logic is skipped if adType is not defined. */ 61 | if ((!targeting || !targeting.hasOwnProperty('position')) && adType !== false) { 62 | const position = this.positions[adType] + 1 || 1; 63 | this.positions[adType] = position; 64 | 65 | const positionParam = Object.assign(targeting, { position }); 66 | Object.assign(params, { targeting: positionParam }); 67 | } 68 | 69 | const prebidEnabled = bidding.prebid && 70 | ((bidding.prebid.enabled && bidding.prebid.bids) || 71 | (typeof bidding.prebid.enabled === 'undefined' && bidding.prebid.bids)); 72 | 73 | if ((isMobile.any() && display === 'mobile') || (!isMobile.any() && display === 'desktop') || (display === 'all')) { 74 | // Registers the advertisement with Prebid.js if enabled on both the unit and wrapper. 75 | if (prebidEnabled && (this.wrapper.prebid && this.wrapper.prebid.enabled) && flatDimensions) { 76 | if (pbjs && iframeBidders.length > 0) { 77 | pbjs.setConfig({ 78 | userSync: { 79 | iframeEnabled: true, 80 | filterSettings: { 81 | iframe: { 82 | bidders: iframeBidders, 83 | filter: 'include' 84 | } 85 | } 86 | } 87 | }); 88 | } 89 | const code = this.wrapper.prebid.useSlotForAdUnit ? determineSlotName(this.dfpId, slotName) : id; 90 | queuePrebidCommand.bind(this, addUnit(code, flatDimensions, bidding.prebid.bids, this.wrapper.prebid, others)); 91 | } 92 | 93 | processDisplayAd = this.displayAd.bind(this, params); 94 | if (processDisplayAd) { 95 | sendLog('registerAd()', 'Queuing Google Tag command for ad', slotName); 96 | queueGoogletagCommand(processDisplayAd); 97 | } 98 | } 99 | } catch (err) { 100 | console.error('ads error', err); 101 | } 102 | } 103 | 104 | /** 105 | * @desc Registers a collection of advertisements. 106 | * @param {array} collection - An array containing a list of objects containing advertisement data. 107 | **/ 108 | registerAdCollection(collection) { 109 | collection.forEach((advert) => { 110 | this.registerAd(advert); 111 | }); 112 | } 113 | 114 | /** 115 | * @desc Registers a collection of advertisements as single prebid and ad calls 116 | * @param {array} collection - An array containing a list of objects containing advertisement data. 117 | **/ 118 | registerAdCollectionSingleCall(collection, bidderTimeout = 700) { 119 | sendLog('registerAdCollectionSingleCall()', 'Registering all reserved ads', null); 120 | 121 | 122 | window.blockArcAdsLoad = true; 123 | window.blockArcAdsPrebid = true; 124 | 125 | collection.forEach((advert) => { 126 | this.registerAd(advert); 127 | }); 128 | 129 | window.blockArcAdsLoad = false; 130 | window.blockArcAdsPrebid = false; 131 | 132 | //prebid call 133 | pbjs.requestBids({ 134 | timeout: bidderTimeout, 135 | //adUnitCodes: codes, 136 | bidsBackHandler: (result) => { 137 | console.log('Bid Back Handler', result); 138 | pbjs.setTargetingForGPTAsync(); 139 | 140 | window.googletag.pubads().refresh(window.adsList); 141 | window.adsList = []; 142 | } 143 | }); 144 | } 145 | 146 | 147 | /** 148 | * @desc Sets blockArcAdsLoad to be true - stops Ad Calls from going out, 149 | * allowing ads to be saved up for a single ad call to be sent out later. 150 | **/ 151 | static setAdsBlockGate() { 152 | const win = ArcAds.getWindow(); 153 | if (typeof win !== 'undefined') { 154 | win.blockArcAdsLoad = true; 155 | } 156 | } 157 | 158 | /** 159 | * @desc Sets blockArcAdsLoad to be true - stops Ad Calls from going out, 160 | * allowing ads to be saved up for a single ad call to be sent out later. 161 | **/ 162 | static releaseAdsBlockGate() { 163 | const win = ArcAds.getWindow(); 164 | if (typeof win !== 'undefined') { 165 | win.blockArcAdsLoad = false; 166 | } 167 | } 168 | 169 | /** 170 | * @desc Displays an advertisement and sets up any neccersary event binding. 171 | * @param {object} params - An object containing all of the function arguments. 172 | * @param {string} params.id - A string containing the advertisement id corresponding to the div the advertisement will load into. 173 | * @param {string} params.slotName - A string containing the slot name of the advertisement, for instance '1234/news/homepage'. 174 | * @param {array} params.dimensions - An array containing all of the applicable sizes the advertisement can use. 175 | * @param {object} params.targeting - An object containing all of the advertisements targeting data. 176 | * @param {array} params.sizemap - An array containing optional size mapping information. 177 | * @param {object} params.bidding - Contains all of the applicable bid data, such as which vendors to use and their placement ids. 178 | * @param {function} params.prerender - An optional function that will run before the advertisement renders. 179 | **/ 180 | displayAd({ 181 | id, 182 | slotName, 183 | dimensions, 184 | targeting, 185 | sizemap = false, 186 | bidding = false, 187 | prerender = null 188 | }) { 189 | const fullSlotName = determineSlotName(this.dfpId, slotName); 190 | const parsedDimensions = dimensions && !dimensions.length ? null : dimensions; 191 | const ad = !dimensions ? window.googletag.defineOutOfPageSlot(fullSlotName, id) 192 | : window.googletag.defineSlot(fullSlotName, parsedDimensions, id); 193 | 194 | if (sizemap && sizemap.breakpoints && dimensions) { 195 | const { mapping, breakpoints, correlators } = prepareSizeMaps(parsedDimensions, sizemap.breakpoints); 196 | 197 | if (ad) { 198 | ad.defineSizeMapping(mapping); 199 | } else { 200 | sendLog('displayAd()', 'No ad available to display - the div was either not defined or an ad with the same slot name already exists on the page', slotName); 201 | return false; 202 | } 203 | 204 | if (sizemap.refresh) { 205 | sendLog('displayAd()', 'Attaching resize listener to the ad with this slot name and sizemap defined', slotName); 206 | setResizeListener({ 207 | ad, 208 | slotName: fullSlotName, 209 | breakpoints, 210 | id, 211 | mapping, 212 | correlators, 213 | bidding, 214 | wrapper: this.wrapper, 215 | prerender 216 | }); 217 | } 218 | } 219 | 220 | if (ad) { 221 | ad.addService(window.googletag.pubads()); 222 | setTargeting(ad, targeting); 223 | } 224 | 225 | const safebreakpoints = (sizemap && sizemap.breakpoints) ? sizemap.breakpoints : []; 226 | 227 | if (window.adsList && ad) { 228 | adsList.push(ad); 229 | } 230 | 231 | if (dimensions && bidding && ((bidding.amazon && bidding.amazon.enabled) || (bidding.prebid && bidding.prebid.enabled))) { 232 | sendLog('displayAd()', 'Fetching bids for ad with this slot name', slotName); 233 | fetchBids({ 234 | ad, 235 | id, 236 | slotName: fullSlotName, 237 | dimensions: parsedDimensions, 238 | wrapper: this.wrapper, 239 | prerender, 240 | bidding, 241 | breakpoints: safebreakpoints 242 | }); 243 | } else if (!window.blockArcAdsPrebid) { 244 | sendLog('displayAd()', 'Refreshing ad with this slot name', slotName); 245 | refreshSlot({ 246 | ad, 247 | prerender, 248 | info: { 249 | adUnit: ad, 250 | adSlot: fullSlotName, 251 | adDimensions: parsedDimensions, 252 | adId: id 253 | } 254 | }); 255 | } 256 | } 257 | 258 | /** 259 | * @desc Send out ads that have been accumulated for the SRA 260 | **/ 261 | sendSingleCallAds(bidderTimeout = 700) { 262 | // if no ads have been accumulated to send out together 263 | // do nothing, return 264 | if (this.adsList && this.adsList.length < 1) { 265 | sendLog('sendSingleCallAds()', 'No ads have been reserved on the page', null); 266 | return false; 267 | } 268 | //ensure library is present and able to send out SRA ads 269 | if (window && window.googletag && googletag.pubadsReady) { // eslint-disable-line 270 | window.googletag.pubads().disableInitialLoad(); 271 | window.googletag.pubads().enableSingleRequest(); 272 | window.googletag.pubads().enableAsyncRendering(); 273 | 274 | this.registerAdCollectionSingleCall(this.adsList, bidderTimeout); 275 | } else { 276 | setTimeout(() => { 277 | this.sendSingleCallAds(); 278 | }, 2000); 279 | } 280 | } 281 | 282 | /** 283 | * Append this ad information to the list of ads 284 | * to be sent out as part of the singleAdCall 285 | * 286 | * @param {Object} params the ad parameters 287 | */ 288 | reserveAd(params) { 289 | ArcAds.setAdsBlockGate(); 290 | this.adsList.push(params); 291 | } 292 | 293 | /** 294 | * Page level targeting - any targeting set 295 | * using this function will apply to all 296 | * ads on the page. This is useful for SRA to 297 | * reduce request length. 298 | * 299 | * @param {string} key Targeting parameter key. 300 | * * @param {string} value Targeting parameter value or array of values. 301 | */ 302 | setPageLeveTargeting(key, value) { //TODO check for pubads 303 | googletag.pubads().setTargeting(key, value); 304 | } 305 | 306 | static getWindow() { 307 | return window; 308 | } 309 | } 310 | -------------------------------------------------------------------------------- /src/__tests__/mobile.test.js: -------------------------------------------------------------------------------- 1 | 2 | import {MobileDetection} from '../util/mobile.js'; 3 | 4 | describe('MobileDetection', () => { 5 | afterAll(() => { 6 | window.__defineGetter__('navigator', function () { 7 | return {}; 8 | }); 9 | 10 | window.navigator.__defineGetter__('userAgent', function () { 11 | return null; 12 | }); 13 | 14 | window.__defineGetter__('retina', function () { 15 | return false; 16 | }); 17 | 18 | window.__defineGetter__('devicePixelRatio', function () { 19 | return 0; 20 | }); 21 | 22 | }); 23 | 24 | describe('Android()', () => { 25 | it ('returns true if user agent contains Android', () => { 26 | window.navigator.__defineGetter__('userAgent', function () { 27 | return 'Android'; 28 | }); 29 | 30 | const result = MobileDetection.Android(); 31 | expect(result).toEqual(true); 32 | }); 33 | 34 | it ('returns false if user agent does not contains Android', () => { 35 | 36 | window.navigator.__defineGetter__('userAgent', function () { 37 | return 'that which shall not be named'; 38 | }); 39 | 40 | const result = MobileDetection.Android(); 41 | expect(result).toEqual(false); 42 | }); 43 | 44 | }); 45 | 46 | describe('AndroidOld()', () => { 47 | 48 | it ('returns true if user agent contains Android 2.3.3', () => { 49 | 50 | window.navigator.__defineGetter__('userAgent', function () { 51 | return 'Android 2.3.3'; 52 | }); 53 | 54 | const result = MobileDetection.AndroidOld(); 55 | expect(result).toEqual(true); 56 | }); 57 | 58 | it ('returns false if user agent does not contain Android 2.3.3', () => { 59 | 60 | window.navigator.__defineGetter__('userAgent', function () { 61 | return 'that which shall not be named'; 62 | }); 63 | 64 | const result = MobileDetection.AndroidOld(); 65 | expect(result).toEqual(false); 66 | }); 67 | 68 | }); 69 | 70 | describe('AndroidTablet()', () => { 71 | 72 | it ('returns true if user agent contains Android Mobile', () => { 73 | 74 | window.navigator.__defineGetter__('userAgent', function () { 75 | return 'Android'; 76 | }); 77 | 78 | const result = MobileDetection.AndroidTablet(); 79 | expect(result).toEqual(true); 80 | }); 81 | 82 | it ('returns false if user agent does not contain Android Mobile', () => { 83 | 84 | window.navigator.__defineGetter__('userAgent', function () { 85 | return 'that which shall not be named'; 86 | }); 87 | 88 | const result = MobileDetection.AndroidTablet(); 89 | expect(result).toEqual(false); 90 | }); 91 | 92 | }); 93 | 94 | describe('Kindle()', () => { 95 | 96 | it ('returns true if user agent contains Kindle', () => { 97 | 98 | window.navigator.__defineGetter__('userAgent', function () { 99 | return 'Kindle'; 100 | }); 101 | 102 | const result = MobileDetection.Kindle(); 103 | expect(result).toEqual(true); 104 | }); 105 | 106 | it ('returns false if user agent does not contain Kindle', () => { 107 | 108 | window.navigator.__defineGetter__('userAgent', function () { 109 | return 'that which shall not be named'; 110 | }); 111 | 112 | const result = MobileDetection.Kindle(); 113 | expect(result).toEqual(false); 114 | }); 115 | 116 | }); 117 | 118 | describe('KindleFire()', () => { 119 | 120 | it ('returns true if user agent contains KFOT', () => { 121 | 122 | window.navigator.__defineGetter__('userAgent', function () { 123 | return 'KFOT'; 124 | }); 125 | 126 | const result = MobileDetection.KindleFire(); 127 | expect(result).toEqual(true); 128 | }); 129 | 130 | it ('returns false if user agent does not contain Kindle', () => { 131 | 132 | window.navigator.__defineGetter__('userAgent', function () { 133 | return 'that which shall not be named'; 134 | }); 135 | 136 | const result = MobileDetection.KindleFire(); 137 | expect(result).toEqual(false); 138 | }); 139 | 140 | }); 141 | 142 | 143 | describe('Silk()', () => { 144 | 145 | it ('returns true if user agent contains Silk', () => { 146 | 147 | window.navigator.__defineGetter__('userAgent', function () { 148 | return 'Silk'; 149 | }); 150 | 151 | const result = MobileDetection.Silk(); 152 | expect(result).toEqual(true); 153 | }); 154 | 155 | it ('returns false if user agent does not contain Silk', () => { 156 | 157 | window.navigator.__defineGetter__('userAgent', function () { 158 | return 'that which shall not be named'; 159 | }); 160 | 161 | const result = MobileDetection.Silk(); 162 | expect(result).toEqual(false); 163 | }); 164 | 165 | }); 166 | 167 | describe('BlackBerry()', () => { 168 | 169 | it ('returns true if user agent contains BlackBerry', () => { 170 | 171 | window.navigator.__defineGetter__('userAgent', function () { 172 | return 'BlackBerry'; 173 | }); 174 | 175 | const result = MobileDetection.BlackBerry(); 176 | expect(result).toEqual(true); 177 | }); 178 | 179 | it ('returns false if user agent does not contain BlackBerry', () => { 180 | 181 | window.navigator.__defineGetter__('userAgent', function () { 182 | return 'that which shall not be named'; 183 | }); 184 | 185 | const result = MobileDetection.BlackBerry(); 186 | expect(result).toEqual(false); 187 | }); 188 | 189 | }); 190 | 191 | describe('iOS()', () => { 192 | 193 | it ('returns true if user agent contains iPhone', () => { 194 | 195 | window.navigator.__defineGetter__('userAgent', function () { 196 | return 'iPhone'; 197 | }); 198 | 199 | const result = MobileDetection.iOS(); 200 | expect(result).toEqual(true); 201 | }); 202 | 203 | it ('returns true if user agent contains iPad', () => { 204 | 205 | window.navigator.__defineGetter__('userAgent', function () { 206 | return 'iPad'; 207 | }); 208 | 209 | const result = MobileDetection.iOS(); 210 | expect(result).toEqual(true); 211 | }); 212 | 213 | it ('returns true if user agent contains iPod', () => { 214 | 215 | window.navigator.__defineGetter__('userAgent', function () { 216 | return 'iPod'; 217 | }); 218 | 219 | const result = MobileDetection.iOS(); 220 | expect(result).toEqual(true); 221 | }); 222 | 223 | it ('returns false if user agent does not contain iPhone/iPad/iPod', () => { 224 | 225 | window.navigator.__defineGetter__('userAgent', function () { 226 | return 'that which shall not be named'; 227 | }); 228 | 229 | const result = MobileDetection.iOS(); 230 | expect(result).toEqual(false); 231 | }); 232 | 233 | }); 234 | 235 | describe('iPhone()', () => { 236 | 237 | it ('returns true if user agent contains iPhone', () => { 238 | 239 | window.navigator.__defineGetter__('userAgent', function () { 240 | return 'iPhone'; 241 | }); 242 | 243 | const result = MobileDetection.iPhone(); 244 | expect(result).toEqual(true); 245 | }); 246 | 247 | 248 | 249 | it ('returns true if user agent contains iPod', () => { 250 | 251 | window.navigator.__defineGetter__('userAgent', function () { 252 | return 'iPod'; 253 | }); 254 | 255 | const result = MobileDetection.iPhone(); 256 | expect(result).toEqual(true); 257 | }); 258 | 259 | it ('returns false if user agent does not contain iPhone/iPad/iPod', () => { 260 | 261 | window.navigator.__defineGetter__('userAgent', function () { 262 | return 'iPad'; 263 | }); 264 | 265 | const result = MobileDetection.iPhone(); 266 | expect(result).toEqual(false); 267 | }); 268 | 269 | }); 270 | 271 | describe('iPad()', () => { 272 | 273 | it ('returns true if user agent contains iPad', () => { 274 | 275 | window.navigator.__defineGetter__('userAgent', function () { 276 | return 'iPad'; 277 | }); 278 | 279 | const result = MobileDetection.iPad(); 280 | expect(result).toEqual(true); 281 | }); 282 | 283 | it ('returns false if user agent does not contain iPad', () => { 284 | 285 | window.navigator.__defineGetter__('userAgent', function () { 286 | return 'that which shall not be named'; 287 | }); 288 | 289 | const result = MobileDetection.iPad(); 290 | expect(result).toEqual(false); 291 | }); 292 | 293 | }); 294 | 295 | describe('Windows()', () => { 296 | 297 | it ('returns true if user agent contains IEMobile', () => { 298 | 299 | window.navigator.__defineGetter__('userAgent', function () { 300 | return 'IEMobile'; 301 | }); 302 | 303 | const result = MobileDetection.Windows(); 304 | expect(result).toEqual(true); 305 | }); 306 | 307 | it ('returns false if user agent does not contain IEMobile', () => { 308 | 309 | window.navigator.__defineGetter__('userAgent', function () { 310 | return 'that which shall not be named'; 311 | }); 312 | 313 | const result = MobileDetection.Windows(); 314 | expect(result).toEqual(false); 315 | }); 316 | 317 | }); 318 | 319 | describe('FirefoxOS()', () => { 320 | 321 | it ('returns true if user agent contains Mozilla and Mobile', () => { 322 | 323 | window.navigator.__defineGetter__('userAgent', function () { 324 | return 'Mozilla Mobile'; 325 | }); 326 | 327 | const result = MobileDetection.FirefoxOS(); 328 | expect(result).toEqual(true); 329 | }); 330 | 331 | it ('returns false if user agent contains Mozilla and not Mobile', () => { 332 | 333 | window.navigator.__defineGetter__('userAgent', function () { 334 | return 'Mozilla'; 335 | }); 336 | 337 | const result = MobileDetection.FirefoxOS(); 338 | expect(result).toEqual(false); 339 | }); 340 | 341 | it ('returns false if user agent contains not Mozilla but Mobile', () => { 342 | 343 | window.navigator.__defineGetter__('userAgent', function () { 344 | return 'Mobile'; 345 | }); 346 | 347 | const result = MobileDetection.FirefoxOS(); 348 | expect(result).toEqual(false); 349 | }); 350 | 351 | it ('returns false if user agent contains neither Mozilla or Mobile', () => { 352 | 353 | window.navigator.__defineGetter__('userAgent', function () { 354 | return 'that which shall not be named'; 355 | }); 356 | 357 | const result = MobileDetection.FirefoxOS(); 358 | expect(result).toEqual(false); 359 | }); 360 | 361 | }); 362 | 363 | describe('any()', () => { 364 | 365 | it ('returns true if user agent contains Android', () => { 366 | 367 | window.navigator.__defineGetter__('userAgent', function () { 368 | return 'Android'; 369 | }); 370 | 371 | const result = MobileDetection.any(); 372 | expect(result).toEqual(true); 373 | }); 374 | 375 | it ('returns true if user agent contains Kindle', () => { 376 | 377 | window.navigator.__defineGetter__('userAgent', function () { 378 | return 'Kindle'; 379 | }); 380 | 381 | const result = MobileDetection.any(); 382 | expect(result).toEqual(true); 383 | }); 384 | 385 | it ('returns true if user agent contains KindleFire', () => { 386 | 387 | window.navigator.__defineGetter__('userAgent', function () { 388 | return 'KFOT'; 389 | }); 390 | 391 | const result = MobileDetection.any(); 392 | expect(result).toEqual(true); 393 | }); 394 | 395 | it ('returns true if user agent contains Silk', () => { 396 | 397 | window.navigator.__defineGetter__('userAgent', function () { 398 | return 'Silk'; 399 | }); 400 | 401 | const result = MobileDetection.any(); 402 | expect(result).toEqual(true); 403 | }); 404 | 405 | it ('returns true if user agent contains BlackBerry', () => { 406 | 407 | window.navigator.__defineGetter__('userAgent', function () { 408 | return 'BlackBerry'; 409 | }); 410 | 411 | const result = MobileDetection.any(); 412 | expect(result).toEqual(true); 413 | }); 414 | 415 | it ('returns true if user agent contains iPad', () => { 416 | 417 | window.navigator.__defineGetter__('userAgent', function () { 418 | return 'iPad'; 419 | }); 420 | 421 | const result = MobileDetection.any(); 422 | expect(result).toEqual(true); 423 | }); 424 | 425 | it ('returns true if user agent contains iPod', () => { 426 | 427 | window.navigator.__defineGetter__('userAgent', function () { 428 | return 'iPod'; 429 | }); 430 | 431 | const result = MobileDetection.any(); 432 | expect(result).toEqual(true); 433 | }); 434 | 435 | it ('returns true if user agent contains iPhone', () => { 436 | 437 | window.navigator.__defineGetter__('userAgent', function () { 438 | return 'iPhone'; 439 | }); 440 | 441 | const result = MobileDetection.any(); 442 | expect(result).toEqual(true); 443 | }); 444 | 445 | it ('returns true if user agent contains IEMobile', () => { 446 | 447 | window.navigator.__defineGetter__('userAgent', function () { 448 | return 'IEMobile'; 449 | }); 450 | 451 | const result = MobileDetection.any(); 452 | expect(result).toEqual(true); 453 | }); 454 | 455 | it ('returns true if user agent contains Mozilla Mobile', () => { 456 | 457 | window.navigator.__defineGetter__('userAgent', function () { 458 | return 'Mozilla Mobile'; 459 | }); 460 | 461 | const result = MobileDetection.any(); 462 | expect(result).toEqual(true); 463 | }); 464 | 465 | it ('returns false if user agent contains invalid mobile userAgent', () => { 466 | 467 | window.navigator.__defineGetter__('userAgent', function () { 468 | return 'no no no'; 469 | }); 470 | 471 | const result = MobileDetection.any(); 472 | expect(result).toEqual(false); 473 | }); 474 | 475 | 476 | 477 | }); 478 | 479 | }); 480 | -------------------------------------------------------------------------------- /dist/arcads.js: -------------------------------------------------------------------------------- 1 | !function(e,n){if("object"==typeof exports&&"object"==typeof module)module.exports=n();else if("function"==typeof define&&define.amd)define([],n);else{var i=n();for(var t in i)("object"==typeof exports?exports:e)[t]=i[t]}}("undefined"!=typeof self?self:this,function(){return function(e){var n={};function i(t){if(n[t])return n[t].exports;var r=n[t]={i:t,l:!1,exports:{}};return e[t].call(r.exports,r,r.exports,i),r.l=!0,r.exports}return i.m=e,i.c=n,i.d=function(e,n,t){i.o(e,n)||Object.defineProperty(e,n,{configurable:!1,enumerable:!0,get:t})},i.n=function(e){var n=e&&e.__esModule?function(){return e.default}:function(){return e};return i.d(n,"a",n),n},i.o=function(e,n){return Object.prototype.hasOwnProperty.call(e,n)},i.p="",i(i.s=5)}([function(e,n,i){"use strict";Object.defineProperty(n,"__esModule",{value:!0}),n.sendLog=function(e,n,i){try{if("true"===new URLSearchParams(window.location.search).get("debug")){var r=(0,t.default)("arcads.js");r({service:"ArcAds",timestamp:""+new Date,"logging from":e,description:n,slotName:i})}}catch(e){console.error(e)}};var t=function(e){return e&&e.__esModule?e:{default:e}}(i(2));i(7)},function(e,n,i){"use strict";Object.defineProperty(n,"__esModule",{value:!0}),n.initializeGPT=function(){window.googletag=window.googletag||{},window.googletag.cmd=window.googletag.cmd||[],(0,t.appendResource)("script","//securepubads.g.doubleclick.net/tag/js/gpt.js",!0,!0),(0,o.sendLog)("initializeGPT()","Appended googletag script to the head tag of the page.",null)},n.refreshSlot=function(e){var n=e.ad,i=e.correlator,t=void 0!==i&&i,r=e.prerender,o=void 0===r?null:r,a=e.info,d=void 0===a?{}:a;new Promise(function(e){if(o)try{o(d).then(function(){e("Prerender function has completed.")})}catch(n){console.warn("ArcAds: Prerender function did not return a promise or there was an error.\n Documentation: https://github.com/washingtonpost/arcads/wiki/Utilizing-a-Prerender-Hook"),e("Prerender function did not return a promise or there was an error, ignoring.")}else e("No Prerender function was provided.")}).then(function(){!function e(){if(window.blockArcAdsLoad)return"blockArcAdsLoad";window.googletag&&googletag.pubadsReady?window.googletag.pubads().refresh([n],{changeCorrelator:t}):setTimeout(function(){e()},200)}()})},n.queueGoogletagCommand=function(e){window.googletag.cmd.push(e)},n.setTargeting=function(e,n){for(var i in n)n.hasOwnProperty(i)&&n[i]&&e.setTargeting(i,n[i])},n.dfpSettings=function(e){window.googletag.pubads().disableInitialLoad(),window.googletag.pubads().enableSingleRequest(),window.googletag.pubads().enableAsyncRendering(),this.collapseEmptyDivs&&((0,o.sendLog)("dfpSettings()","This wrapper is set to collapse any empty divs.",null),window.googletag.pubads().collapseEmptyDivs());window.googletag.enableServices(),e&&((0,o.sendLog)("dfpSettings()","This wrapper has a function to call upon the slot render ending.",null),window.googletag.pubads().addEventListener("slotRenderEnded",e))},n.determineSlotName=function(e,n){var i=(0,r.expandQueryString)("adslot");if(i&&(""!==i||null!==i))return"/"+e+"/"+i;return"/"+e+"/"+n};var t=i(8),r=i(9),o=i(0)},function(e,n){var i=Object.create(null),t=function(e,n){return e?i[e]||(i[e]=t.ext(t.new(e,n))):i};t.levels={error:1,warn:2,info:3,log:4,debug:5,trace:6},t.new=function(e,n){var i={};i[e]=function(){t.log(e,[].slice.call(arguments))};try{Object.defineProperty(i[e],"name",{get:function(){return e}})}catch(e){}return i[e]},t.log=function(e,n){var r=n.length>1&&t.levels[n[0]]?n.shift():"log";i[e][r].apply(i[e],n)},t.ext=function(e){for(var n in e.enabledFor=function(){},t.levels)e[n]=function(){};return e},e.exports=t},function(e,n,i){"use strict";Object.defineProperty(n,"__esModule",{value:!0}),n.initializeBiddingServices=d,n.fetchBids=function(e){var n=this,i=e.ad,a=e.id,s=e.slotName,l=e.dimensions,u=e.wrapper,c=e.bidding,p=e.correlator,g=void 0!==p&&p,f=e.prerender,h=e.breakpoints,b={adUnit:i,adSlot:s,adDimensions:l,adId:a,bids:c},v=new Promise(function(e){if(u.prebid&&u.prebid.enabled){var r=u.prebid.timeout||700;t.queuePrebidCommand.bind(n,(0,t.fetchPrebidBids)(i,u.prebid.useSlotForAdUnit?s:a,r,b,f,function(){e("Fetched Prebid ads!")}))}else e("Prebid is not enabled on the wrapper...")}),w=new Promise(function(e){u.amazon&&u.amazon.enabled?(0,r.fetchAmazonBids)(a,s,l,h,function(){e("Fetched Amazon ads!")}):e("Amazon is not enabled on the wrapper...")});window.arcBiddingReady?Promise.all([v,w]).then(function(){(0,o.refreshSlot)({ad:i,correlator:g,prerender:f,info:b})}):setTimeout(function(){return d()},200)};var t=i(4),r=i(10),o=i(1),a=i(0);function d(e){var n=e.prebid,i=void 0!==n&&n,t=e.amazon,o=void 0!==t&&t;if(window.arcBiddingReady)(0,a.sendLog)("initializeBiddingServices()","Header bidding has been previously initialized",null);else{window.arcBiddingReady=!1;var d=new Promise(function(e){if(i&&i.enabled){if("undefined"==typeof pbjs){var n=n||{};n.que=n.que||[]}e("Prebid has been initialized")}else(0,a.sendLog)("initializeBiddingServices()","Prebid is not enabled on this wrapper.",null),e("Prebid is not enabled on the wrapper...")}),s=new Promise(function(e){o&&o.enabled&&window.apstag?o.id&&""!==o.id?(0,r.queueAmazonCommand)(function(){window.apstag.init({pubID:o.id,adServer:"googletag"}),e("Amazon scripts have been added onto the page!")}):(console.warn("ArcAds: Missing Amazon account id. \n Documentation: https://github.com/washingtonpost/arcads#amazon-tama9"),(0,a.sendLog)("initializeBiddingServices()","Amazon is not enabled on this wrapper.",null),e("Amazon is not enabled on the wrapper...")):e("Amazon is not enabled on the wrapper...")});Promise.all([d,s]).then(function(){window.arcBiddingReady=!0})}}},function(e,n,i){"use strict";Object.defineProperty(n,"__esModule",{value:!0});var t=Object.assign||function(e){for(var n=1;n5&&void 0!==arguments[5]?arguments[5]:null,d=t;d.bids=Array.isArray(t.bids)?t.bids:[t.bids],o(e,[n],i,d,r,a)},n.addUnit=function(e,n,i){var r=arguments.length>3&&void 0!==arguments[3]?arguments[3]:{},o=arguments.length>4&&void 0!==arguments[4]?arguments[4]:{},a=t({code:e,bids:i},o);a.mediaTypes={banner:{sizes:n}};var d=r.sizeConfig,s=r.config;if(pbjs.addAdUnits(a),s)return void pbjs.setConfig(s);d&&pbjs.setConfig({sizeConfig:d})};var r=i(1);function o(e,n,i,t,o){var a=arguments.length>5&&void 0!==arguments[5]?arguments[5]:null;pbjs.addAdUnits(t),window.blockArcAdsPrebid||pbjs.requestBids({timeout:i,adUnitCodes:n,bidsBackHandler:function(i){console.log("Bid Back Handler",i),pbjs.setTargetingForGPTAsync(n),a?a():(0,r.refreshSlot)({ad:e,info:t,prerender:o})}})}},function(e,n,i){"use strict";Object.defineProperty(n,"__esModule",{value:!0}),n.ArcAds=void 0;var t=function(){function e(e,n){for(var i=0;i1&&void 0!==arguments[1]?arguments[1]:null;!function(e,n){if(!(e instanceof n))throw new TypeError("Cannot call a class as a function")}(this,e),this.dfpId=n.dfp.id||"",this.wrapper=n.bidding||{},this.positions=[],this.collapseEmptyDivs=n.dfp.collapseEmptyDivs,this.adsList=[],window.isMobile=r.MobileDetection,""===this.dfpId?(console.warn("ArcAds: DFP id is missing from the arcads initialization script.","\n","Documentation: https://github.com/washingtonpost/arcads#getting-started"),(0,o.sendLog)("constructor()","The DFP id missing from the arcads initialization script. ArcAds cannot proceed.",null)):((0,d.initializeGPT)(),(0,d.queueGoogletagCommand)(d.dfpSettings.bind(this,i)),(0,a.initializeBiddingServices)(this.wrapper))}return t(e,[{key:"registerAd",value:function(e){var n=e.id,i=e.slotName,t=e.dimensions,r=e.adType,a=void 0!==r&&r,l=e.targeting,c=void 0===l?{}:l,p=e.display,g=void 0===p?"all":p,f=e.bidding,h=void 0!==f&&f,b=e.iframeBidders,v=void 0===b?["openx"]:b,w=e.others,m=void 0===w?{}:w,y=[],A=!1,k=function e(n){return Array.isArray(n)?1+Math.max.apply(Math,u(n.map(function(n){return e(n)}))):0}(t);t&&void 0!==t&&1===k?y.push.apply(y,u(t)):t&&void 0!==t&&t.length>0&&2===k?y.push.apply(y,u(t)):t&&t.forEach(function(e){y.push.apply(y,u(e))});try{if(!(c&&c.hasOwnProperty("position")||!1===a)){var P=this.positions[a]+1||1;this.positions[a]=P;var z=Object.assign(c,{position:P});Object.assign(e,{targeting:z})}var S=h.prebid&&(h.prebid.enabled&&h.prebid.bids||void 0===h.prebid.enabled&&h.prebid.bids);if(isMobile.any()&&"mobile"===g||!isMobile.any()&&"desktop"===g||"all"===g){if(S&&this.wrapper.prebid&&this.wrapper.prebid.enabled&&y){pbjs&&v.length>0&&pbjs.setConfig({userSync:{iframeEnabled:!0,filterSettings:{iframe:{bidders:v,filter:"include"}}}});var L=this.wrapper.prebid.useSlotForAdUnit?(0,d.determineSlotName)(this.dfpId,i):n;s.queuePrebidCommand.bind(this,(0,s.addUnit)(L,y,h.prebid.bids,this.wrapper.prebid,m))}(A=this.displayAd.bind(this,e))&&((0,o.sendLog)("registerAd()","Queuing Google Tag command for ad",i),(0,d.queueGoogletagCommand)(A))}}catch(e){console.error("ads error",e)}}},{key:"registerAdCollection",value:function(e){var n=this;e.forEach(function(e){n.registerAd(e)})}},{key:"registerAdCollectionSingleCall",value:function(e){var n=this,i=arguments.length>1&&void 0!==arguments[1]?arguments[1]:700;(0,o.sendLog)("registerAdCollectionSingleCall()","Registering all reserved ads",null),window.blockArcAdsLoad=!0,window.blockArcAdsPrebid=!0,e.forEach(function(e){n.registerAd(e)}),window.blockArcAdsLoad=!1,window.blockArcAdsPrebid=!1,pbjs.requestBids({timeout:i,bidsBackHandler:function(e){console.log("Bid Back Handler",e),pbjs.setTargetingForGPTAsync(),window.googletag.pubads().refresh(window.adsList),window.adsList=[]}})}},{key:"displayAd",value:function(e){var n=e.id,i=e.slotName,t=e.dimensions,r=e.targeting,s=e.sizemap,u=void 0!==s&&s,c=e.bidding,p=void 0!==c&&c,g=e.prerender,f=void 0===g?null:g,h=(0,d.determineSlotName)(this.dfpId,i),b=t&&!t.length?null:t,v=t?window.googletag.defineSlot(h,b,n):window.googletag.defineOutOfPageSlot(h,n);if(u&&u.breakpoints&&t){var w=(0,l.prepareSizeMaps)(b,u.breakpoints),m=w.mapping,y=w.breakpoints,A=w.correlators;if(!v)return(0,o.sendLog)("displayAd()","No ad available to display - the div was either not defined or an ad with the same slot name already exists on the page",i),!1;v.defineSizeMapping(m),u.refresh&&((0,o.sendLog)("displayAd()","Attaching resize listener to the ad with this slot name and sizemap defined",i),(0,l.setResizeListener)({ad:v,slotName:h,breakpoints:y,id:n,mapping:m,correlators:A,bidding:p,wrapper:this.wrapper,prerender:f}))}v&&(v.addService(window.googletag.pubads()),(0,d.setTargeting)(v,r));var k=u&&u.breakpoints?u.breakpoints:[];window.adsList&&v&&adsList.push(v),t&&p&&(p.amazon&&p.amazon.enabled||p.prebid&&p.prebid.enabled)?((0,o.sendLog)("displayAd()","Fetching bids for ad with this slot name",i),(0,a.fetchBids)({ad:v,id:n,slotName:h,dimensions:b,wrapper:this.wrapper,prerender:f,bidding:p,breakpoints:k})):window.blockArcAdsPrebid||((0,o.sendLog)("displayAd()","Refreshing ad with this slot name",i),(0,d.refreshSlot)({ad:v,prerender:f,info:{adUnit:v,adSlot:h,adDimensions:b,adId:n}}))}},{key:"sendSingleCallAds",value:function(){var e=this,n=arguments.length>0&&void 0!==arguments[0]?arguments[0]:700;if(this.adsList&&this.adsList.length<1)return(0,o.sendLog)("sendSingleCallAds()","No ads have been reserved on the page",null),!1;window&&window.googletag&&googletag.pubadsReady?(window.googletag.pubads().disableInitialLoad(),window.googletag.pubads().enableSingleRequest(),window.googletag.pubads().enableAsyncRendering(),this.registerAdCollectionSingleCall(this.adsList,n)):setTimeout(function(){e.sendSingleCallAds()},2e3)}},{key:"reserveAd",value:function(n){e.setAdsBlockGate(),this.adsList.push(n)}},{key:"setPageLeveTargeting",value:function(e,n){googletag.pubads().setTargeting(e,n)}}],[{key:"setAdsBlockGate",value:function(){var n=e.getWindow();void 0!==n&&(n.blockArcAdsLoad=!0)}},{key:"releaseAdsBlockGate",value:function(){var n=e.getWindow();void 0!==n&&(n.blockArcAdsLoad=!1)}},{key:"getWindow",value:function(){return window}}]),e}()},function(e,n,i){"use strict";Object.defineProperty(n,"__esModule",{value:!0});var t=function(){function e(e,n){for(var i=0;i1}},{key:"any",value:function(){return this.Android()||this.Kindle()||this.KindleFire()||this.Silk()||this.BlackBerry()||this.iOS()||this.Windows()||this.FirefoxOS()}}]),e}();n.default=r},function(e,n,i){var t=function(e){return e&&"object"==typeof e&&"default"in e?e:{default:e}}(i(2));t.default.ext=function(e){var n="undefined"!=typeof console&&console;for(var i in t.default.levels)e[i]=n&&(n[i]||n.log)||function(){};return e.enabledFor=function(){return!0},e}},function(e,n,i){"use strict";Object.defineProperty(n,"__esModule",{value:!0}),n.appendResource=function(e,n,i,t,r){var o=document.createElement(e);if("script"!==e)return;o.src=n,o.async=i||!1,o.defer=i||t||!1;(document.head||document.documentElement).appendChild(o),r&&r()}},function(e,n,i){"use strict";Object.defineProperty(n,"__esModule",{value:!0}),n.expandQueryString=function(e){var n=window.location.href,i=e.replace(/[[\]]/g,"\\$&"),t=new RegExp("[?&]"+i+"(=([^&#]*)|&|#|$)").exec(n);if(!t)return null;if(!t[2])return"";return decodeURIComponent(t[2].replace(/\+/g," "))}},function(e,n,i){"use strict";function t(e){window.apstag&&e()}Object.defineProperty(n,"__esModule",{value:!0}),n.fetchAmazonBids=function(e,n,i,r){var o=arguments.length>4&&void 0!==arguments[4]?arguments[4]:null,a=i;if(r&&void 0!==window.innerWidth&&void 0!==i[0][0][0]){for(var d=window.innerWidth,s=-1,l=r.length,u=0;u=r[u][0]){s=u;break}a=i[s]}t(function(){var i={slotName:n,slotID:e,sizes:a};window.apstag.fetchBids({slots:[i]},function(){window.apstag.setDisplayBids(),o&&o()})})},n.queueAmazonCommand=t},function(e,n,i){"use strict";Object.defineProperty(n,"__esModule",{value:!0}),n.resizeListeners=n.sizemapListeners=void 0,n.prepareSizeMaps=function(e,n){var i=[],t=[],r=[],o=n.length?n:null;o&&e&&o.forEach(function(n,o){i.push([n,e[o]]),-1===t.indexOf(n[0])&&(t.push(n[0]),r.push(!1))});return t.sort(function(e,n){return e-n}),{mapping:i,breakpoints:t,correlators:r}},n.parseSizeMappings=l,n.runResizeEvents=u,n.setResizeListener=function(e){var n=e.id,i=e.correlators;s[n]=(0,t.debounce)(u(e),250),window.addEventListener("resize",s[n]),d[n]={listener:s[n],correlators:i}};var t=i(12),r=i(3),o=i(1),a=i(0),d=n.sizemapListeners={},s=n.resizeListeners={};function l(e){try{var n=[window.innerWidth||document.documentElement.clientWidth||document.body.clientWidth,window.innerHeight||document.documentElement.clientHeight||document.body.clientHeight],i=e.filter(function(e){return e[0][0]<=n[0]&&e[0][1]<=n[1]}),t=i.length>0?i[0][1]:[];return t.length>0&&t[0].constructor!==Array&&(t=[t]),t}catch(n){return(0,a.sendLog)("parseSizeMappings()","invalid size mapping",null),e[e.length-1][1]}}function u(e){var n=void 0,i=!1;if(e.breakpoints){var t=window.innerWidth;n=e.breakpoints.filter(function(e){return eb&&(h 11 | 12 | 20 | ``` 21 | 22 | `collapseEmptyDivs` is an optional parameter that directly toggles `googletag.pubads().collapseEmptyDivs()` 23 | 24 | Alternatively, if you're using a bundler you can use the library as a module. 25 | 26 | ``` 27 | npm install arcads 28 | ``` 29 | 30 | You can then include it in your own JavaScript projects like so. 31 | 32 | ```javascript 33 | import { ArcAds } from 'arcads' 34 | ``` 35 | 36 | ## Displaying an Advertisement 37 | You can display an advertisement by calling the `registerAd` method, this can be called as many times and wherever you'd like as required. 38 | 39 | ```javascript 40 | arcAds.registerAd({ 41 | id: 'div-id-123', 42 | slotName: 'hp/hp-1', 43 | dimensions: [[300, 250], [300, 600]], 44 | display: 'desktop' 45 | }) 46 | ``` 47 | 48 | Along with the `registerAd` call you also need a div on the page with the same id. 49 | 50 | ```html 51 |
52 | ``` 53 | 54 | If you are using an external service to manage initial ad load (like Didomi), set `window.blockArcAdsLoad = true` on page load to block ArcAds from refreshing ads. Set `window.blockArcAdsLoad = false` when you want ArcAds to control refreshing ads again. 55 | 56 | The following table shows all of the possible parameters the `registerAd` method accepts. 57 | 58 | | Parameter | Description | Type | Requirement | 59 | | ------------- | ------------- | ------------- | ------------- | 60 | | `id` | The `id` parameter corresponds to a div id on the page that the advertisement should render into. | `String` | `Required` | 61 | | `slotName` | The `slotName` parameter is equal to the slot name configured within GPT, for example `sitename/hp/hp-1`. The publisher ID gets attached to the slot name within the ArcAds logic. | `String` | `Required` | 62 | | `dimensions` | The `dimensions` parameter should be an array with array of arrays containing the advertisement sizes the slot can load. If left empty the advertisement will be considered as an out of page unit. | `Array` | `Optional` | 63 | | `adType` | The `adType` parameter should describe the type of advertisement, for instance `leaderboard` or `cube`. | `String` | `Optional` | 64 | | `display` | The `display` parameter determines which user agents can render the advertisement. The available choices are `desktop`, `mobile`, or `all`. If a value is not provided it will default to `all`. | `String` | `Optional` | 65 | | `targeting` | The `targeting` parameter accepts an object containing key/value pairs which should attached to the advertisement request. | `Object` | `Optional` | 66 | | `sizemap` | The `sizemap` parameter accepts an object containing information about the advertisements size mapping, for more information refer to the [Size Mapping portion of the readme](https://github.com/washingtonpost/arcads#size-mapping). | `Object` | `Optional` | 67 | | `bidding` | The `bidding` parameter accepts an object containing information about the advertisements header bidding vendors, for more information refer to the [Header Bidding portion of the readme](https://github.com/washingtonpost/arcads#header-bidding). | `Object` | `Optional` | 68 | | `prerender` | The `prerender` parameter accepts an a function that should fire before the advertisement loads, for more information refer to the [Prerender Hook portion of the readme](https://github.com/washingtonpost/arcads/tree/master#prerender-hook). | `Function` | `Optional` | 69 | 70 | ### Out of Page Ads 71 | If an advertisement has an empty or missing `dimensions` parameter it will be considered as a [GPT Out of Page creative](https://support.google.com/admanager/answer/6088046?hl=en) and rendered as such. 72 | 73 | ### Callback 74 | Whenever an advertisement loads you can access data about the advertisement such as its size and id by passing in an optional callback to the initialization of ArcAds. This ties a handler to the `slotRenderEnded` event that GPT emits and is called every time an advertisement is about to render, allowing you to make any page layout modifications to accommodate a specific advertisement. 75 | 76 | ```javascript 77 | const arcAds = new ArcAds({ 78 | dfp: { 79 | id: '123' 80 | } 81 | }, (event) => { 82 | console.log('Advertisement has loaded...', event) 83 | }) 84 | ``` 85 | 86 | #### Refreshing an Advertisement 87 | If you require the ability to refresh a specific advertisement you can do so via the googletag library, providing it the slot object from GPT. You can get access to the slot object in the callback of ArcAds via `event.slot`. 88 | 89 | ```javascript 90 | const arcAds = new ArcAds({ 91 | dfp: { 92 | id: '123' 93 | } 94 | }, (event) => { 95 | window.adSlot = event.slot 96 | }) 97 | 98 | // Refresh a single ad slot 99 | window.googletag.pubads().refresh([window.adSlot]) 100 | 101 | // Refresh all ad slots on the page 102 | window.googletag.pubads().refresh() 103 | ``` 104 | 105 | ### Targeting 106 | Advertisement targeting parameters can be passed to the registration call via the `targeting` object. 107 | 108 | ```javascript 109 | arcAds.registerAd({ 110 | id: 'div-id-123', 111 | slotName: 'hp/hp-1', 112 | adType: 'cube', 113 | dimensions: [[300, 250], [300, 600]], 114 | display: 'all', 115 | targeting: { 116 | section: 'weather' 117 | } 118 | }) 119 | ``` 120 | 121 | The service will automatically give the advertisement a `position` target key/value pair if either the `targeting` object or `position` key of the targeting object are not present. The position value will increment by 1 in sequence for each of the same `adType` on the page. This is a common practice between ad traffickers so this behavior is baked in, only if the trafficker makes use of this targeting will it have any effect on the advertisement rendering. 122 | 123 | If `adType` is excluded from the `registerAd` call the automatic position targeting will not be included. 124 | 125 | ## Size Mapping 126 | You can configure GPT size mapped ads with the same registration call by adding a `sizemap` object. To utilize size mapping the `dimensions` key should be updated to include an array representing a nested array of arrays containing the applicable sizes for a specific breakpoint. 127 | 128 | ```javascript 129 | [ [[970, 250], [970, 90], [728, 90]], 130 | [[728, 90]], 131 | [[320, 100], [320, 50]] ] 132 | ``` 133 | 134 | Followed by an array of equal lengths of breakpoints which will sit within `sizemap.breakpoints`. 135 | 136 | ```javascript 137 | [ [1280, 0], [800, 0], [0, 0] ] 138 | ``` 139 | 140 | When put together this will mean that at a window width of 1280 wide, the service can load a `970x250`, `970x90` or a `728x90` advertisement. At 800 wide, it can load a `728x90`, and anything below 800 it will load a `320x90` or a `320x50`. 141 | 142 | If the advertisement should refresh dynamically when the user resizes the screen after the initial load you can toggle `refresh` to `true`. otherwise it should be `false`. 143 | 144 | ```javascript 145 | arcAds.registerAd({ 146 | id: 'div-id-123', 147 | slotName: 'hp/hp-1', 148 | adType: 'cube', 149 | dimensions: [ [[970, 250], [970, 90], [728, 90]], [[728, 90]], [[320, 100], [320, 50]] ], 150 | targeting: { 151 | section: 'weather' 152 | }, 153 | sizemap: { 154 | breakpoints: [ [1280, 0], [800, 0], [0, 0] ], 155 | refresh: true 156 | } 157 | }) 158 | ``` 159 | 160 | ## Prerender Hook 161 | ArcAds provides a way for you to get information about an advertisement before it loads, which is useful for attaching targeting data from third party vendors. 162 | 163 | You can setup a function within the `registerAd` call by adding a `prerender` parameter, the value of which being the function you'd like to fire before the advertisement loads. This function will also fire before the advertisement refreshes if you're using sizemapping. 164 | 165 | ```javascript 166 | arcAds.registerAd({ 167 | id: 'div-id-123', 168 | slotName: 'hp/hp-1', 169 | dimensions: [[300, 250], [300, 600]], 170 | display: 'desktop', 171 | prerender: window.adFunction 172 | }) 173 | ``` 174 | 175 | Your `prerender` function must return a promise. Once it's resolved the advertisement will display. If you do not resolve the promise the advertisement will *not* render. 176 | 177 | ```javascript 178 | window.adFunction = function(ad) { 179 | return new Promise(function(resolve, reject) { 180 | // The 'ad' arguement will provide information about the unit 181 | console.log(ad) 182 | // If you do not resolve the promise the advertisement will not display 183 | resolve() 184 | }); 185 | } 186 | ``` 187 | 188 | You can gather information about the advertisement by accessing the `ad` argument/object. 189 | 190 | | Key | Description | 191 | | ------------- | ------------- | 192 | | `adUnit` | An object containing the GPT ad slot. This can be used when calling other GPT methods. | 193 | | `adSlot` | Contains a string with the full slot name of the advertisement. | 194 | | `adDimensions` | Contains an array with the size of the advertisement which is about to load. | 195 | | `adId` | Contains a string with the id of the advertisement. | 196 | 197 | For a more detailed example of how to utilize this functionality [please see the wiki](https://github.com/washingtonpost/arcads/wiki/Utilizing-a-Prerender-Hook). 198 | 199 | ## Header Bidding 200 | ArcAds supports Prebid.js and Amazon TAM/A9. To enable these services you must first enable them when you configure the wrapper. 201 | 202 | ### Prebid.js 203 | If you'd like to include Prebid.js you must include the library before `arcads.js`. You can get a customized version of Prebid.js with the adapters your site needs from their website [here](http://prebid.org/download.html). 204 | 205 | ```javascript 206 | 207 | 208 | 209 | 221 | 222 | ``` 223 | You can enable Prebid.js on the wrapper by adding a `prebid` object to the wrapper initialization and setting `enabled: true`. If `enabled` is `undefined`, `prebid` can still be used by providing a valid `bids` object. You can also optionally pass it a `timeout` value which corresponds in milliseconds how long Prebid.js will wait until it closes out the bidding for the advertisements on the page. By default, the timeout will be set to `700`. 224 | 225 | ```javascript 226 | const arcAds = new ArcAds({ 227 | dfp: { 228 | id: '123' 229 | }, 230 | bidding: { 231 | prebid: { 232 | enabled: true, 233 | timeout: 1000 234 | } 235 | } 236 | } 237 | ``` 238 | 239 | If you want to use the slotName instead of the ad id when registering ads, pass `useSlotForAdUnit: true`. 240 | 241 | ```javascript 242 | const arcAds = new ArcAds({ 243 | dfp: { 244 | id: '123' 245 | }, 246 | bidding: { 247 | prebid: { 248 | enabled: true, 249 | timeout: 1000, 250 | useSlotForAdUnit: true 251 | } 252 | } 253 | } 254 | ``` 255 | 256 | On the wrapper you can also configure a size mapping configuration, which will provide information to Prebid.js on which sized advertisements it should fetch bids for on each breakpoint. For more information on what needs to be configured within the `sizeConfig` array click [here](http://prebid.org/dev-docs/examples/size-mapping.html). 257 | 258 | ```javascript 259 | const arcAds = new ArcAds({ 260 | dfp: { 261 | id: '123' 262 | }, 263 | bidding: { 264 | prebid: { 265 | enabled: true, 266 | timeout: 1000, 267 | sizeConfig: [ 268 | { 269 | 'mediaQuery': '(min-width: 1024px)', 270 | 'sizesSupported': [ 271 | [970, 250], 272 | [970, 90], 273 | [728, 90] 274 | ], 275 | 'labels': ['desktop'] 276 | }, 277 | { 278 | 'mediaQuery': '(min-width: 480px) and (max-width: 1023px)', 279 | 'sizesSupported': [ 280 | [728, 90] 281 | ], 282 | 'labels': ['tablet'] 283 | }, 284 | { 285 | 'mediaQuery': '(min-width: 0px)', 286 | 'sizesSupported': [ 287 | [320, 100], 288 | [320, 50] 289 | ], 290 | 'labels': ['phone'] 291 | } 292 | ] 293 | } 294 | } 295 | }) 296 | ``` 297 | 298 | On the advertisement registration you can then provide information about which bidding services that specific advertisement should use. You can find a list of parameters that Prebid.js accepts for each adapter on the [Prebid.js website](http://prebid.org/dev-docs/publisher-api-reference.html). Additionally you can turn on [Prebid.js debugging](http://prebid.org/dev-docs/toubleshooting-tips.html) by adding `?pbjs_debug=true` to the url. 299 | 300 | ```javascript 301 | arcAds.registerAd({ 302 | id: 'div-id-123', 303 | slotName: 'hp/hp-1', 304 | adType: 'cube', 305 | display: 'desktop', 306 | dimensions: [ [[970, 250], [970, 90], [728, 90]], [[728, 90]], [[320, 100], [320, 50]] ], 307 | sizemap: { 308 | breakpoints: [ [1280, 0], [800, 0], [0, 0] ], 309 | refresh: 'true' 310 | }, 311 | bidding: { 312 | prebid: { 313 | enabled: true, 314 | bids: [{ 315 | bidder: 'appnexus', 316 | labels: ['desktop', 'tablet', 'phone'], 317 | params: { 318 | placementId: '10433394' 319 | } 320 | }] 321 | } 322 | } 323 | }) 324 | ``` 325 | 326 | ### Amazon TAM/A9 327 | You can enable Amazon A9/TAM on the service by adding an `amazon` object to the wrapper initialization and then passing it `enabled: true`. You must also include the `apstag` script on your page with: 328 | ``` 329 | 330 | ``` 331 | 332 | You must also provide your publication id that corresponds to the owners Amazon account. 333 | 334 | ```javascript 335 | const arcAds = new ArcAds({ 336 | dfp: { 337 | id: '123' 338 | }, 339 | bidding: { 340 | amazon: { 341 | enabled: true, 342 | id: '123' 343 | } 344 | } 345 | }) 346 | ``` 347 | 348 | On the advertisement registration you simply provide `enabled: true` for the specific advertisement within the `bidding` object. There are no additional properties which are required. 349 | 350 | ```javascript 351 | arcAds.registerAd({ 352 | id: 'div-id-123', 353 | slotName: 'hp/hp-1', 354 | adType: 'cube', 355 | display: 'desktop', 356 | dimensions: '[ [[970, 250], [970, 90], [728, 90]], [[728, 90]], [[320, 100], [320, 50]] ]', 357 | sizemap: { 358 | breakpoints: '[ [1280, 0], [800, 0], [0, 0] ]', 359 | refresh: 'true' 360 | }, 361 | bidding: { 362 | amazon: { 363 | enabled: true 364 | } 365 | } 366 | }) 367 | ``` 368 | 369 | NOTE: Currently Amazon A9/TAM is not supported for use with Singe Request Architecture (SRA). 370 | 371 | ## Registering Multiple Ads 372 | You can display multiple ads at once using the `registerAdCollection` method. This is useful if you're initializing multiple advertisements at once in the page header. To do this you can pass an array of advertisement objects similar to the one you would with the `registerAd` call. Note that when using this function, if setAdsBlockGate() has not been called, the calls for each ad will be made individually. If you need to achieve Single Request Architecture, see the documentation below, "SRA Single Request Architecture". 373 | 374 | ```javascript 375 | const ads = [{ 376 | id: 'div-id-123', 377 | slotName: 'hp/hp-1', 378 | adType: 'cube', 379 | display: 'desktop', 380 | dimensions: '[ [[970, 250], [970, 90], [728, 90]], [[728, 90]], [[320, 100], [320, 50]] ]', 381 | sizemap: { 382 | breakpoints: '[ [1280, 0], [800, 0], [0, 0] ]', 383 | refresh: 'true' 384 | }, 385 | bidding: { 386 | prebid: { 387 | enabled: true, 388 | bids: [{ 389 | bidder: 'appnexus', 390 | labels: ['desktop', 'tablet', 'phone'], 391 | params: { 392 | placementId: '10433394' 393 | } 394 | }] 395 | } 396 | }, 397 | { 398 | id: 'div-id-456', 399 | slotName: 'hp/hp-2', 400 | adType: 'square', 401 | display: 'mobile', 402 | dimensions: '[ [300, 250], [300, 600] ]', 403 | bidding: { 404 | prebid: { 405 | enabled: true, 406 | bids: [{ 407 | bidder: 'appnexus', 408 | labels: ['desktop', 'tablet', 'phone'], 409 | params: { 410 | placementId: '10433394' 411 | } 412 | }] 413 | } 414 | } 415 | ] 416 | 417 | 418 | arcAds.registerAdCollection(ads) 419 | ``` 420 | 421 | ## SRA Single Request Architecture 422 | SRA architecture Functions will allow all ads to go out in one single ad call. The functions are presented in the order they should be called: 423 | 424 | 1. setPageLevelTargeting(key, value): sets targeting parameters- applied to all ads on the page. Extracting common targeting values is recommended in order to avoid repeating targeting for each ad in the single ad call. 425 | 1. setAdsBlockGate(): “closes” the gate - as ads are added, calls do not go out. This allows ads configurations to accumulated to be set out later, together all at once. 426 | 1. reserveAd(params): accumulates ads to be sent out later. This function is called once per one ad. 427 | 1. releaseAdsBlockGate(): “opens” the gate - allows an ad call to go out. 428 | 1. sendSingleCallAds(): registers all the ads added via reserveAd(), and sends out a single ad call (SRA call) containing all the ads information that has been added so far via reserveAd(). 429 | 430 | To add more ads, repeat steps 1-5 as needed. 431 | 432 | NOTE: Prebid is supported for SRA. Amazon A9/TAM is not supported for SRA and will need to be implemented at a future date. 433 | 434 | NOTE: ArcAds SRA implementation calls enableSingleRequest() which means that when using pubads lazyLoad functions together with SRA, when the first ad slot comes within the viewport specified by the fetchMarginPercent parameter, the call for that ad and all other ad slots is made. If different behavior is desired after the initial SRA call is made, an outside lazy loading library may be used to manage the calls for regsterAd, reserveAd and other calls. 435 | 436 | ## Developer Tools 437 | There's a series developer tools available, to get started run `npm install`. 438 | 439 | | Command | Description | 440 | | ------------- | ------------- | 441 | | `npm run dev` | Runs the development command, watches for changes and compiles the changes down to the `dist` directory. | 442 | | `npm run build` | Builds the project into the `dist` directory with minification. | 443 | | `npm run docs` | Generates ESDoc documentation in the `docs` directory on-demand. | 444 | | `npm run test` | Runs a series of unit tests with Jest. Tests are automatically validated during a pull request. | 445 | | `npm run debug` | Starts a local http server so you can link directly to the script during development. For example ` | 446 | 447 | ### Slot Override 448 | You can override the slot name of every advertisement on the page by appending `?adslot=` to the URL. This will override whatever is placed inside of the `slotName` field when invoking the `registerAd` method. For example, if you hit the URL `arcpublishing.com/?adslot=homepage/myad`, the full ad slot path will end up being your GPT id followed by the value: `123/homepage/myad`. 449 | 450 | You can also debug slot names and GPT in general by typing `window.googletag.openConsole()` into the browsers developer console. 451 | 452 | ### Logging 453 | To inspect the funtion calls taking place within the ArcAds library, you can include the `debug=true` query parameter on your page. 454 | 455 | ## Contributing 456 | If you'd like to contribute to ArcAds please read our [contributing guide](https://github.com/washingtonpost/ArcAds/blob/master/CONTRIBUTING.md). 457 | --------------------------------------------------------------------------------