diff --git a/CONTRIBUTING.md b/.github/CONTRIBUTING.md similarity index 100% rename from CONTRIBUTING.md rename to .github/CONTRIBUTING.md diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..963f11e --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,27 @@ +## What does this Pull Request do? + + + +## Why is this change being made? + + + +## Where should the reviewer start? + + + +## How was this tested? How can the reviewer verify your testing? + + +.spec.js`.> + +## What gif best describes this PR or how it makes you feel? + +![](SOMEURL) + +## Completion checklist + +- [ ] The pull request has been appropriately labelled according to our conventions. +- [ ] Documentation has been updated. +- [ ] The change has unit & integration tests as appropriate. + diff --git a/.jscsrc b/.jscsrc index 4f1204d..958f086 100644 --- a/.jscsrc +++ b/.jscsrc @@ -1,5 +1,6 @@ { "preset": "google", "maximumLineLength": null, - "validateQuoteMarks": { "mark": "'", "escape": true } + "validateQuoteMarks": { "mark": "'", "escape": true }, + "jsDoc": false } diff --git a/README.md b/README.md index c783807..45f4b97 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,6 @@ [![Dependencies](https://img.shields.io/david/dev/krux/gpt-mock.svg)](./package.json) [![Commitizen friendly](https://img.shields.io/badge/commitizen-friendly-brightgreen.svg)](http://commitizen.github.io/cz-cli/) [![semantic-release](https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg)](https://github.com/semantic-release/semantic-release) -[![Gitter](https://badges.gitter.im/krux/gpt-mock.svg)](https://gitter.im/krux/gpt-mock) A test library to mock out the Google Publisher Tag library. @@ -75,7 +74,7 @@ worked on, please [file a new issue](https://github.com/krux/gpt-mock/issues/new We ♥ [forks and pull requests](https://help.github.com/articles/using-pull-requests). -Please see [CONTRIBUTING.md](CONTRIBUTING.md) for full details. +Please see [CONTRIBUTING.md](./.github/CONTRIBUTING.md) for full details. ## Environment @@ -124,7 +123,7 @@ npm run tdd ## Issue Guidelines -Please either add a failing [unit test](./test/unit) or include a [jsfiddle](http://jsfiddle.net) that distills and reproduces the issue. +Please add a failing [bug test](./test/bugs) and create a Pull Request. # License diff --git a/esdoc.json b/esdoc.json new file mode 100644 index 0000000..7fdfe13 --- /dev/null +++ b/esdoc.json @@ -0,0 +1,27 @@ +{ + "title": "gpt-mock", + "destination": "./docs", + "lint": true, + "manual": { + "overview": ["./manual/Overview.md"], + "installation": ["./manual/Installation.md"], + "example": ["./manual/Example.md"] + }, + "test": { + "type": "mocha", + "source": "./test/unit" + }, + "plugins": [ + { + "name": "esdoc-importpath-plugin", + "option": { + "replaces": [ + { + "from": "^src/", + "to": "dist/" + } + ] + } + } + ] +} diff --git a/gulpfile.babel.js b/gulpfile.babel.js index 7744552..9a3b168 100644 --- a/gulpfile.babel.js +++ b/gulpfile.babel.js @@ -13,6 +13,7 @@ import eslint from 'gulp-eslint'; import jscs from 'gulp-jscs'; import header from 'gulp-header'; import esdoc from 'gulp-esdoc'; +import esdocConfig from './esdoc.json'; const DIST = 'dist'; @@ -68,9 +69,7 @@ gulp.task('build', build(webpackConfig)); gulp.task('doc', () => { return gulp.src('./src') - .pipe(esdoc({ - destination: './doc' - })); + .pipe(esdoc(esdocConfig)); }); gulp.task('lint', ['lint:eslint', 'lint:jscs']); diff --git a/manual/Example.md b/manual/Example.md new file mode 100644 index 0000000..b319e30 --- /dev/null +++ b/manual/Example.md @@ -0,0 +1,18 @@ +# Example + +```javascript +import GPT from 'gpt-mock'; + +googletag = new GPT(); +googletag.cmd.push(function() { + googletag.defineSlot('/Test/12345', [728, 90], 'gpt-div-1').addService(googletag.pubads()); +}); + +googletag.cmd.push(function() { + googletag.display('gpt-div-1'); +}); + +// Vitally, you need to tell the library that it has loaded so the commands +// will be executed. +googletag._loaded(); +``` diff --git a/manual/Installation.md b/manual/Installation.md new file mode 100644 index 0000000..22c13db --- /dev/null +++ b/manual/Installation.md @@ -0,0 +1,6 @@ +# Installation + +## From npm +```bash +npm install -D gpt-mock +``` diff --git a/manual/Overview.md b/manual/Overview.md new file mode 100644 index 0000000..7416618 --- /dev/null +++ b/manual/Overview.md @@ -0,0 +1,12 @@ +# Overview + +`gpt-mock` is a testing library for mocking out the `Google Publisher Tag` (`GPT`) library, so you +can test your site or library for how it interacts with `GPT` without using the production library. + +## Author + +Seth Yates + +## License + +[Apache 2](../LICENSE) diff --git a/package.json b/package.json index 5352cfc..205ae65 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "description": "Test library for mocking out Google Publisher Tags", "homepage": "https://github.com/krux/gpt-mock/", "bugs": "https://github.com/krux/gpt-mock/issues", - "license": "Apache-2", + "license": "Apache-2.0", "keywords": [ "gpt", "dfp", @@ -65,14 +65,15 @@ "babel-plugin-transform-es3-member-expression-literals": "6.8.0", "babel-plugin-transform-es3-property-literals": "6.8.0", "babel-plugin-transform-object-assign": "6.8.0", - "babel-plugin-transform-runtime": "6.12.0", + "babel-plugin-transform-runtime": "6.15.0", "babel-preset-es2015": "6.14.0", - "babel-preset-es2015-loose": "7.0.0", + "babel-preset-es2015-loose": "8.0.0", "babel-register": "6.14.0", "babelify": "7.3.0", "cz-conventional-changelog": "1.2.0", "del": "2.2.2", - "eslint": "3.4.0", + "esdoc-importpath-plugin": "0.0.1", + "eslint": "3.5.0", "expect.js": "0.3.1", "gulp": "3.9.1", "gulp-babel": "6.1.2", @@ -86,25 +87,25 @@ "istanbul": "0.4.5", "jscs": "3.0.7", "json-loader": "0.5.4", - "karma": "1.2.0", + "karma": "1.3.0", "karma-babel-preprocessor": "6.0.1", "karma-coverage": "1.1.1", "karma-coveralls": "1.1.2", "karma-expect": "1.1.2", "karma-mocha": "1.1.1", "karma-mocha-reporter": "2.1.0", - "karma-phantomjs-launcher": "1.0.1", + "karma-phantomjs-launcher": "1.0.2", "karma-sinon": "1.0.5", "karma-webpack": "1.8.0", "lolex": "1.5.1", "mocha": "3.0.2", "phantomjs-prebuilt": "2.1.12", - "process": "0.11.8", - "semantic-release": "^4.3.5", + "process": "0.11.9", + "semantic-release": "4.3.5", "sinon": "1.17.5", "watchify": "3.7.0", "webpack": "1.13.2", - "webpack-dev-server": "1.15.0", + "webpack-dev-server": "1.15.2", "webpack-stream": "3.2.0" } } diff --git a/src/CommandArray.js b/src/CommandArray.js index 51336a2..03d61dd 100644 --- a/src/CommandArray.js +++ b/src/CommandArray.js @@ -5,7 +5,7 @@ */ export default class CommandArray { /** - * Creates a new CommandArray. + * Creates a new {@link CommandArray}. * * @param {Array} commands The commands to execute */ @@ -21,11 +21,13 @@ export default class CommandArray { * * @param {function()} f A JavaScript function to be executed. * @returns {number} The number of commands processed so far. This is - * compatible with Array.push's return value (the current length of the array). + * compatible with {@link Array#push}'s return value (the current length of the array). */ push(f) { - f(); - this._count += 1; + if (f != null && typeof f === 'function') { + f(); + this._count += 1; + } return this._count; } } diff --git a/src/CompanionAdsService.js b/src/CompanionAdsService.js index 7c60599..5065d45 100644 --- a/src/CompanionAdsService.js +++ b/src/CompanionAdsService.js @@ -7,26 +7,32 @@ export default class CompanionAdsService extends Service { /** * Creates a new CompanionAdsService. * - * @param {GPT} gt The containing GPT instance. + * @param {GPT} gpt The containing {@link GPT} instance. */ - constructor(gt) { - super(gt, CompanionAdsService._name); + constructor(gpt) { + super(gpt, CompanionAdsService._name); this._options = { syncLoading: false, refreshUnfilledSlots: null }; } + /** + * The name of the service. + * + * @type {string} + * @private + */ static get _name() { return 'companion_ads'; } /** * Enables the service implementation to be loaded synchronously. This needs - * to be called before {@link GPT#enableServices()}. + * to be called before {@link GPT#enableServices}. * * Note: this call can be only used if gpt.js is also loaded synchronously, - * for example, by using a script element. If called when GPT is loaded + * for example, by using a script element. If called when {@link GPT} is loaded * asynchronously, the outcome of the loading is undefined. */ enableSyncLoading() { @@ -35,7 +41,7 @@ export default class CompanionAdsService extends Service { /** * Sets whether companion slots that have not been filled will be automatically - * backfilled. Only slots that are also registered with the pubads service will + * backfilled. Only slots that are also registered with the {@link PubAdsService} will * be backfilled. This method can be called multiple times during the page's * lifetime to turn backfill on and off. * diff --git a/src/ContentService.js b/src/ContentService.js index 3311704..9c84b66 100644 --- a/src/ContentService.js +++ b/src/ContentService.js @@ -5,20 +5,32 @@ import Service from './Service'; */ export default class ContentService extends Service { /** - * Creates a new ContentService. + * Creates a new {@link ContentService}. * - * @param {GPT} gt The containing GPT instance. + * @param {GPT} gpt The containing {@link GPT} instance. */ - constructor(gt) { - super(gt, ContentService._name); + constructor(gpt) { + super(gpt, ContentService._name); this._storedContent = []; } + /** + * The name of the service. + * + * @type {string} + * @private + */ static get _name() { return 'content'; } - _onEnable() { + /** + * Enables the service. + * + * @override + */ + enable() { + super.enable(); for (let [slot, content] of this._storedContent) { slot._setContent(content); } diff --git a/src/GPT.js b/src/GPT.js index 6170d36..84f7ebd 100644 --- a/src/GPT.js +++ b/src/GPT.js @@ -6,38 +6,39 @@ import SizeMappingBuilder from './SizeMappingBuilder'; import CommandArray from './CommandArray'; /** - * This is the global namespace that the GPT uses for its API. + * This is the global namespace that the {@link GPT} uses for its API. * * For details on this API, see https://developers.google.com/doubleclick-gpt/reference. * - * @namespace googletag + * Note that the recommended way of handling async is to use {@link GPT#cmd} to + * queue callbacks for when {@link GPT} is ready. These callbacks do not have to check + * {@link GPT#apiReady} as they are guaranteed to execute once the API is set up. * - * @property {boolean|undefined} apiReady Flag indicating that GPT API is loaded - * and ready to be called. This property will be simply undefined until the API - * is ready. - * - * Note that the recommended way of handling async is to use {@link #cmd} to - * queue callbacks for when GPT is ready. These callbacks do not have to check - * {@link #apiReady} as they are guaranteed to execute once the API is set up. - * - * @property {boolean|undefined} pubadsReady Flag indicating that Pubads service - * is enabled, loaded and fully operational. - * This property will be simply `undefined` until {@link #enableServices()} - * is called and Pubads service is loaded and initialized. - * - * @property {!Array|!CommandArray} cmd Reference to the global - * command queue for asynchronous execution of GPT-related calls. - * - * The {@link #cmd} variable is initialized to an empty JavaScript array by + * The {@link GPT#cmd} variable is initialized to an empty JavaScript array by * the GPT tag syntax on the page, and cmd.push is the standard Array.push - * method that adds an element to the end of the array. When the GPT JavaScript + * method that adds an element to the end of the array. When the {@link GPT} JavaScript * is loaded, it looks through the array and executes all the functions in - * order. The script then replaces cmd with a CommandArray object + * order. The script then replaces cmd with a {@link CommandArray} object * whose push method is defined to execute the function argument passed to it. - * This mechanism allows GPT to reduce perceived latency by fetching the + * This mechanism allows {@link GPT} to reduce perceived latency by fetching the * JavaScript asynchronously while allowing the browser to continue rendering * the page. * + * @example + * import GPT from 'gpt-mock'; + * + * googletag = new GPT(); + * googletag.cmd.push(function() { + * googletag.defineSlot('/Test/12345', [728, 90], 'gpt-div-1').addService(googletag.pubads()); + * }); + * + * googletag.cmd.push(function() { + * googletag.display('gpt-div-1'); + * }); + * + * // Vitally, you need to tell the library that it has loaded so the commands + * // will be executed. + * googletag._loaded(); */ export default class GPT { /** @@ -46,12 +47,35 @@ export default class GPT { * @param {number} version The version to emulate. */ constructor(version = 94) { + /** + * Flag indicating that {@link GPT} API is loaded and ready to be called. + * This property will be simply undefined until the API is ready. + * + * @type {boolean|undefined} + */ this.apiReady = void 0; + + /** + * Flag indicating that {@link PubAdsService} is enabled, loaded and fully + * operational. + * + * This property will be simply `undefined` until {@link GPT#enableServices()} + * is called and {@link PubAdsService} is loaded and initialized. + * + * @type {boolean|undefined} + */ this.pubadsReady = void 0; + + /** + * Reference to the global command queue for asynchronous execution of + * {@link GPT}-related calls. + * + * @type {!Array|!CommandArray} + */ this.cmd = []; this._version = version; this._slots = []; - this._slotsByDomId = {}; + this._slotCounter = 0; this._services = {}; this._addService(new CompanionAdsService(this)); this._addService(new ContentService(this)); @@ -60,7 +84,7 @@ export default class GPT { } /** - * Returns the current version of GPT. + * Returns the current version of {@link GPT}. * * @returns {string} Version string. */ @@ -69,34 +93,34 @@ export default class GPT { } /** - * Returns a reference to the companion ads service. + * Returns a reference to the {@link CompanionAdsService}. * - * @returns {CompanionAdsService} Instance of the companion ads service. + * @returns {CompanionAdsService} Instance of the {@link CompanionAdsService}. */ companionAds() { return this._services[CompanionAdsService._name]; } /** - * Returns a reference to the content service. + * Returns a reference to the {@link ContentService}. * - * @returns {ContentService} Instance of the content service. + * @returns {ContentService} Instance of the {@link ContentService}. */ content() { return this._services[ContentService._name]; } /** - * Returns a reference to the pubads service. + * Returns a reference to the {@link PubAdsService}. * - * @returns {PubAdsService} Instance of the pubads service. + * @returns {PubAdsService} Instance of the {@link PubAdsService}. */ pubads() { return this._services[PubAdsService._name]; } /** - * Enables all GPT services that have been defined for ad slots on the page. + * Enables all {@link GPT} services that have been defined for ad slots on the page. */ enableServices() { for (let service in this._services) { @@ -107,7 +131,7 @@ export default class GPT { } /** - * Creates a new SizeMappingBuilder. + * Creates a new {@link SizeMappingBuilder}. * * @returns {SizeMappingBuilder} A new builder. */ @@ -123,11 +147,12 @@ export default class GPT { * @param {GeneralSize} size Width and height of the added slot. This is the * size that is used in the ad request if no responsive size mapping is provided * or the size of the viewport is smaller than the smallest size provided in the mapping. - * @param {string=} optDiv ID of the div that will contain this ad unit. + * @param {string} [optDiv] ID of the div that will contain this ad unit. * @returns {Slot} The newly created slot. */ defineSlot(adUnitPath, size, optDiv) { - return this._addSlot(new Slot(adUnitPath, size, optDiv)); + this._slotCounter += 1; + return this._addSlot(new Slot(adUnitPath, size, optDiv, this._slotCounter)); } /** @@ -135,26 +160,27 @@ export default class GPT { * optDiv is the ID of the div element that will contain the ad. * * @param {string} adUnitPath Full path of the ad unit with the network code and ad unit code. - * @param {string=} optDiv ID of the div that will contain this ad unit. + * @param {string} [optDiv] ID of the div that will contain this ad unit. * @returns {Slot} The newly created slot. */ defineOutOfPageSlot(adUnitPath, optDiv) { - const slot = new Slot(adUnitPath, [], optDiv); + this._slotCounter += 1; + const slot = new Slot(adUnitPath, [], optDiv, this._slotCounter); slot._outOfPage = true; return this._addSlot(slot); } /** * Destroys the given slots, removes all related objects and references of - * given slots from GPT. This API does not support passback slots and + * given slots from {@link GPT}. This API does not support passback slots and * companion slots. Calling this API clears the ad and removes the slot object - * from the internal state maintained by GPT. Calling any more functions on + * from the internal state maintained by {@link GPT}. Calling any more functions on * that slot object will result in undefined behaviour. Note the browser may * still not free the memory associated with that slot if a reference to it is * maintained by the publisher page. Calling this API makes the div associated * with that slot available for reuse. * - * @param {Array=} optSlots The array of slots to + * @param {Array} [optSlots] The array of slots to * destroy. Array is optional; all slots will be destroyed if it is unspecified. * @returns {boolean} true if slots have been destroyed, false otherwise. */ @@ -163,7 +189,7 @@ export default class GPT { const i = this._slots.indexOf(slot); if (i !== -1) { slot._removeServices(); - this._slots.splice(i, 1); + this._removeSlot(slot); } else { return false; } @@ -182,20 +208,19 @@ export default class GPT { * * If single request architecture (SRA) is being used, all unfetched ad slots * at the moment display is called will be fetched in a single instance of - * {@link #display()}. To force an ad slot not to display, the entire div + * {@link GPT#display}. To force an ad slot not to display, the entire div * must be removed. * * @param {string} div ID of the div element containing the ad slot. */ display(div) { if (div) { - if (this._slotsByDomId[div]) { - this._slotsByDomId[div].display(); - } else { - // TODO - error + for (let slot of this._slots) { + if (slot.getSlotElementId() === div) { + slot.display(); + return; + } } - } else { - // TODO - error } } @@ -210,7 +235,7 @@ export default class GPT { disablePublisherConsole() {} /** - * Sets that title for all ad container iframes created by pubads service, + * Sets that title for all ad container iframes created by {@link PubAdsService}, * from this point onwards. * * @param {string} title The title to set. @@ -219,19 +244,38 @@ export default class GPT { this._title = title; } + /** + * Adds the given {@link Service}. + * + * @private + * @param {Service} service The service to add + * @returns {Service} The service added + */ _addService(service) { this._services[service.getName()] = service; return service; } + /** + * Adds the given {@link Slot}. + * + * @private + * @param {Slot} slot The {@link Slot} to add. + * @returns {Slot} The {@link Slot} added. + */ _addSlot(slot) { if (this._slots.indexOf(slot) === -1) { this._slots.push(slot); - this._slotsByDomId[slot.getSlotElementId()] = slot; } return slot; } + /** + * Removes the given {@link Slot}. + * + * @private + * @param {Slot} slot The {@link Slot} to remove. + */ _removeSlot(slot) { const index = this._slots.indexOf(slot); if (index !== -1) { @@ -239,6 +283,12 @@ export default class GPT { } } + /** + * Tells the {@link GPT} service that it has finished loading. This method + * MUST be called in order for most of the rest of the library to function. + * + * @public + */ _loaded() { if (!this.apiReady) { this.apiReady = true; diff --git a/src/GeneralSize.js b/src/GeneralSize.js index 553d648..f534676 100644 --- a/src/GeneralSize.js +++ b/src/GeneralSize.js @@ -1,5 +1,7 @@ /** - * Represents either a single [W, H] size, named size or an array of sizes. + * Represents either a [W, H] {@link SingleSize}, {@link NamedSize} or {@link MultiSize}. + * + * @private * * @typedef {SingleSize|MultiSize} GeneralSize */ @@ -9,17 +11,22 @@ import * as MultiSize from './MultiSize'; /** * Returns true if the given object is a {@link GeneralSize}. * + * @private + * * @param {GeneralSize|*} obj The object to test */ export function isGeneralSize(obj) { - return SingleSize.isSingleSize(obj) || MultiSize.isMultiSize(obj); + return obj != null && (SingleSize.isSingleSize(obj) || MultiSize.isMultiSize(obj)); } /** * Returns an array of {@link Size} instances from the {@link GeneralSize}. * + * @private + * * @param {GeneralSize|*} obj The object to convert. - * @returns {Array} The Size instance or null if the object is not a {@link GeneralSize}. + * @returns {Array} The {@link Size} instances or empty array if the object + * is not a {@link GeneralSize}. */ export function toSizes(obj) { if (obj == null || (Array.isArray(obj) && obj.length == 0)) { diff --git a/src/MultiSize.js b/src/MultiSize.js index 82443bf..7658f78 100644 --- a/src/MultiSize.js +++ b/src/MultiSize.js @@ -1,11 +1,13 @@ /** * @typedef {Array} MultiSize + * @private */ import * as SingleSize from './SingleSize'; /** - * Returns true if the given object is a MultiSize. + * Returns true if the given object is a {@link MultiSize}. * + * @private * @param {MultiSize|*} obj The object to test */ export function isMultiSize(obj) { @@ -25,8 +27,10 @@ export function isMultiSize(obj) { /** * Returns an array of {@link Size} instances from the {@link MultiSize}. * + * @private * @param {MultiSize|*} obj The object to convert. - * @returns {Array} The Size instance or null if the object is not a {@link MultiSize}. + * @returns {Array} The {@link Size} instances or empty array if the object + * is not a {@link MultiSize}. */ export function toSizes(obj) { const acc = []; diff --git a/src/NamedSize.js b/src/NamedSize.js index 767a98c..d700064 100644 --- a/src/NamedSize.js +++ b/src/NamedSize.js @@ -9,11 +9,13 @@ * https://support.google.com/dfp_premium/answer/6366905). * * @typedef {string} NamedSize + * @private */ /** * Returns true if the given object is a {@link NamedSize}. * + * @private * @param {NamedSize|*} obj The object to test */ export function isNamedSize(obj) { diff --git a/src/PubAdsService.js b/src/PubAdsService.js index be92011..8a9fe7f 100644 --- a/src/PubAdsService.js +++ b/src/PubAdsService.js @@ -1,19 +1,21 @@ import Service from './Service'; import Slot from './Slot'; +import TargetingMap from './TargetingMap'; /** - * Publisher Ads service. This service is used to fetch and show ads from your DFP account. + * Publisher Ads service. This service is used to fetch and show ads from your + * {@link DFP} account. */ export default class PubAdsService extends Service { /** * Creates a new PubAdsService. * - * @param {GPT} gt The containing GPT instance. + * @param {GPT} gt The containing {@link GPT} instance. */ constructor(gt) { super(gt, PubAdsService._name); this._categoryExclusions = []; - this._targeting = {}; + this._targeting = new TargetingMap(); this._options = { collapseEmptyDivs: false, collapseBeforeAdFetch: false, @@ -34,12 +36,24 @@ export default class PubAdsService extends Service { this._correlator = Math.random(); } + /** + * The name of the service. + * + * @private + * @type {string} + */ static get _name() { return 'publisher_ads'; } - _onEnable() { - this._gt.pubadsReady = true; + /** + * Enables the service. + * + * @override + */ + enable() { + super.enable(); + this._gpt.pubadsReady = true; } /** @@ -48,12 +62,12 @@ export default class PubAdsService extends Service { * * For proper behavior across all browsers, calling refresh must be preceded * by a call to display the ad slot. If the call to display is omitted, refresh - * may behave unexpectedly. If desired, the disableInitialLoad method can be + * may behave unexpectedly. If desired, the {@link PubAdsService#disableInitialLoad} method can be * used to stop display from fetching an ad. * - * @param {Array=} optSlots The slots to refresh. Array is optional; + * @param {Array} [optSlots] The slots to refresh. Array is optional; * all slots will be refreshed if it is unspecified. - * @param {{changeCorrelator: boolean}=} optOptions Configuration options + * @param {{changeCorrelator: boolean}} [optOptions] Configuration options * associated with this refresh call. changeCorrelator specifies whether or * not a new correlator is to be generated for fetching ads. Our ad servers * maintain this correlator value briefly (currently for 30 seconds, but @@ -78,7 +92,7 @@ export default class PubAdsService extends Service { * Removes the ads from the given slots and replaces them with blank * content. The slots will be marked as unfetched. * - * @param {Array=} optSlots The array of slots to clear. Array is + * @param {Array} [optSlots] The array of slots to clear. Array is * optional; all slots will be cleared if it is unspecified. * @returns {boolean} Returns true if slots have been cleared, false otherwise. */ @@ -100,9 +114,9 @@ export default class PubAdsService extends Service { } /** - * Constructs an out-of-page passback slot. A passback is where a GPT snippet + * Constructs an out-of-page passback slot. A passback is where a {@link GPT} snippet * is used as a creative in an ad serving system. The ad serving system could - * be DFP or a third party. In this case, the ads are always requested + * be {@link DFP} or a third party. In this case, the ads are always requested * synchronously in non-single request mode. * * @param {string} adUnitPath The ad unit path of the slot to use as a passback. @@ -121,8 +135,8 @@ export default class PubAdsService extends Service { } /** - * Constructs a passback slot. A passback is where a GPT snippet is used as a - * creative in an ad serving system. The ad serving system could be DFP or a + * Constructs a passback slot. A passback is where a {@link GPT} snippet is used as a + * creative in an ad serving system. The ad serving system could be {@link DFP} or a * third party. In this case, the ads are always requested synchronously in * non-single request mode. * @@ -143,12 +157,14 @@ export default class PubAdsService extends Service { /** * Disables requests for ads on page load, but allows ads to be requested with - * a GPT#pubads().refresh() call. This should be set prior to enabling + * a {@link PubAdsService#refresh} call. This should be set prior to enabling * the service. Async mode must be used; otherwise it will be impossible to - * request ads using `refresh`. + * request ads using {@link PubAdsService#refresh}. */ disableInitialLoad() { - this._options.initialLoad = false; + this._unlessEnabled(() => { + this._options.initialLoad = false; + }); } /** @@ -162,18 +178,15 @@ export default class PubAdsService extends Service { * was called after the service was enabled. */ enableAsyncRendering() { - if (this._enabled) { - return false; - } else { + return this._unlessEnabled(() => { this._options.asyncRendering = true; - return true; - } + }); } /** * Enables single request mode for fetching multiple ads at the same time. - * This requires all pubads slots to be defined and added to the pubads - * service prior to enabling the service. Single request mode must be set + * This requires all pubads slots to be defined and added to the {@link PubAdsService} + * prior to enabling the service. Single request mode must be set * before the service is enabled. * * @returns {boolean} Returns true if single request mode was enabled and false @@ -181,34 +194,28 @@ export default class PubAdsService extends Service { * called after the service was enabled. */ enableSingleRequest() { - if (this._enabled) { - return false; - } else { + return this._unlessEnabled(() => { this._options.singleRequest = true; - return true; - } + }); } /** * Enables sync rendering mode to enable blocking fetching and rendering of ads. * This mode must be set before the service is enabled. Synchronous rendering - * also requires that the GPT JavaScript be loaded synchronously. + * also requires that the {@link GPT} JavaScript be loaded synchronously. * * @returns {boolean} Returns true if sync rendering mode was enabled and false * if it is impossible to enable sync rendering mode because the method was * called after the service was enabled. */ enableSyncRendering() { - if (this._enabled) { - return false; - } else { + return this._unlessEnabled(() => { this._options.syncRendering = true; - return true; - } + }); } /** - * Signals to GPT that video ads will be present on the page. This enables + * Signals to {@link GPT} that video ads will be present on the page. This enables * competitive exclusion constraints on display and video ads. If the video * content is known, call setVideoContent in order to be able to use content * exclusion for display ads. @@ -220,18 +227,14 @@ export default class PubAdsService extends Service { /** * Sets custom targeting parameters for a given key that apply to all pubads * service ad slots. Calling this multiple times for the same key will - * overwrite old values. These keys are defined in your DFP account. + * overwrite old values. These keys are defined in your {@link DFP} account. * * @param {string} key Targeting parameter key. * @param {string|!Array} value Targeting parameter value or array of values. * @returns {PubAdsService} The service object on which the method was called. */ setTargeting(key, value) { - if (Array.isArray(value)) { - this._targeting[key] = value; - } else { - this._targeting[key] = [value]; - } + this._targeting.set(key, value); return this; } @@ -243,7 +246,7 @@ export default class PubAdsService extends Service { * array if there is no such key. */ getTargeting(key) { - return this._targeting[key] || []; + return this._targeting.get(key); } /** @@ -252,7 +255,7 @@ export default class PubAdsService extends Service { * @returns {!Array} Array of targeting keys. Ordering is undefined. */ getTargetingKeys() { - return Object.keys(this._targeting); + return this._targeting.keys(); } /** @@ -262,7 +265,7 @@ export default class PubAdsService extends Service { * @returns {PubAdsService} The service object on which the method was called. */ clearTargeting(key) { - delete this._targeting[key]; + this._targeting.clear(key); return this; } @@ -271,21 +274,18 @@ export default class PubAdsService extends Service { * page when there is no ad content to display. This mode must be set before * the service is enabled. * - * @param {boolean=} optCollapseBeforeAdFetch Whether to collapse the slots + * @param {boolean} [optCollapseBeforeAdFetch=false] Whether to collapse the slots * even before the ads are fetched. This parameter is optional; if not * provided, false will be used as the default value. * @returns {boolean} Returns true if div collapse mode was enabled and false * if it is impossible to enable collapse mode because the method was called * after the service was enabled. */ - collapseEmptyDivs(optCollapseBeforeAdFetch) { - if (this._enabled) { - return false; - } else { + collapseEmptyDivs(optCollapseBeforeAdFetch = false) { + return this._unlessEnabled(() => { this._options.collapseEmptyDivs = true; this._options.collapseBeforeAdFetch = optCollapseBeforeAdFetch; - return true; - } + }); } /** @@ -300,7 +300,8 @@ export default class PubAdsService extends Service { } /** - * Clears all page-level ad category exclusion labels. This is useful if you want to refresh the slot. + * Clears all page-level ad category exclusion labels. + * This is useful if you want to refresh the slot. * * @returns {PubAdsService} The service object on which the method was called. */ @@ -309,19 +310,16 @@ export default class PubAdsService extends Service { return this; } - _getCategoryExclusions() { - return this._categoryExclusions.slice(0); - } - /** * Enables/disables centering of ads. This mode must be set before the service - * is enabled. Centering is disabled by default. In legacy gpt_mobile.js, - * centering is enabled by default. + * is enabled. Centering is disabled by default. * * @param {boolean} centerAds true to center ads, false to left-align them. */ setCentering(centerAds) { - this._options.centerAds = centerAds; + this._unlessEnabled(() => { + this._options.centerAds = centerAds; + }); } /** @@ -337,7 +335,8 @@ export default class PubAdsService extends Service { } /** - * Configures whether all ads on the page should be forced to be rendered using a SafeFrame container. + * Configures whether all ads on the page should be forced to be rendered using + * a SafeFrame container. * * @param {boolean} forceSafeFrame true to force all ads on the page to be * rendered in SafeFrames and false to change the previous setting to false. @@ -351,12 +350,12 @@ export default class PubAdsService extends Service { /** * Passes location information from websites so you can geo-target line items - * to specific locations. DFP will not use location data unless this feature + * to specific locations. {@link DFP} will not use location data unless this feature * has been enabled for your network. * * @param {string|number} latitudeOrAddress Latitude or freeform address. - * @param {number=} optLongitude The longitude (if a latitude was provided as first argument). - * @param {number=} optRadius The radius in millimeters. Will be rounded to closest integer. + * @param {number} [optLongitude] The longitude (if a latitude was provided as first argument). + * @param {number} [optRadius] The radius in millimeters. Will be rounded to closest integer. * Only used when passing the latitude and longitude. * @returns {PubAdsService} The service object on which the method was called. */ @@ -429,7 +428,7 @@ export default class PubAdsService extends Service { * Sets the video content information to be sent along with the ad requests for * targeting and content exclusion purposes. Video ads will be automatically * enabled when this method is called. For videoContentId and videoCmsId, use - * the values that are provided to the DFP content ingestion service. + * the values that are provided to the {@link DFP} content ingestion service. * * @param {string} videoContentId The video content ID. * @param {string} videoCmsId The video CMS ID. @@ -450,4 +449,20 @@ export default class PubAdsService extends Service { return this; } + /** + * Conditionally execute a function unless the service has already been + * enabled. + * + * @param {function} fn The function to call if the service is not enabled + * @returns {boolean} true if the function was called; false if the service has + * already been enabled. + */ + _unlessEnabled(fn) { + if (this._enabled) { + return false; + } else { + fn(); + return true; + } + } } diff --git a/src/ResponseInformation.js b/src/ResponseInformation.js index 1412048..ab79c37 100644 --- a/src/ResponseInformation.js +++ b/src/ResponseInformation.js @@ -3,13 +3,13 @@ */ export default class ResponseInformation { /** - * Creates a new ResponseInformation. + * Creates a new {@link ResponseInformation}. * * @param {string} advertiserId The ID of the advertiser. * @param {string} campaignId The ID of the campaign. * @param {?number} lineItemId The ID of the line item. * @param {?number} creativeId The ID of the creative. - * @param {?Array=} labelIds The label IDs of the creative. + * @param {?Array} [labelIds] The label IDs of the creative. */ constructor(advertiserId, campaignId, lineItemId, creativeId, labelIds) { this._advertiserId = advertiserId; @@ -22,7 +22,7 @@ export default class ResponseInformation { /** * The ID of the advertiser. * - * @returns {string} + * @type {string} */ get advertiserId() { return this._advertiserId; @@ -31,7 +31,7 @@ export default class ResponseInformation { /** * The ID of the campaign. * - * @returns {string} + * @type {string} */ get campaignId() { return this._campaignId; @@ -40,7 +40,7 @@ export default class ResponseInformation { /** * The ID of the line item. * - * @returns {?number} + * @type {?number} */ get lineItemId() { return this._lineItemId; @@ -49,7 +49,7 @@ export default class ResponseInformation { /** * The ID of the creative. * - * @returns {?number} + * @type {?number} */ get creativeId() { return this._creativeId; @@ -58,7 +58,7 @@ export default class ResponseInformation { /** * The label IDs of the creative. * - * @returns {?Array.} + * @type {?Array} */ get labelIds() { return this._labelIds; diff --git a/src/SafeFrameConfig.js b/src/SafeFrameConfig.js index 50e109f..c7f4d4e 100644 --- a/src/SafeFrameConfig.js +++ b/src/SafeFrameConfig.js @@ -3,7 +3,7 @@ */ export default class SafeFrameConfig { /** - * Creates a new SafeFrameConfig + * Creates a new {@link SafeFrameConfig} * * @param {boolean} allowOverlayExpansion true to allow expansion by overlay and false otherwise. * @param {boolean} allowPushExpansion true to allow expansion by push and false otherwise. @@ -19,7 +19,7 @@ export default class SafeFrameConfig { /** * true to allow expansion by overlay and false otherwise. * - * @returns {boolean} + * @type {boolean} */ get allowOverlayExpansion() { return this._allowOverlayExpansion; @@ -28,7 +28,7 @@ export default class SafeFrameConfig { /** * true to allow expansion by push and false otherwise. * - * @returns {boolean} + * @type {boolean} */ get allowPushExpansion() { return this._allowPushExpansion; @@ -41,7 +41,7 @@ export default class SafeFrameConfig { * and creatives that attempt to navigate the top level page instead of opening * in a new window. * - * @returns {boolean} boolean + * @type {boolean} boolean */ get sandbox() { return this._sandbox; diff --git a/src/Service.js b/src/Service.js index 7ee1b9a..9624bad 100644 --- a/src/Service.js +++ b/src/Service.js @@ -3,14 +3,14 @@ */ export default class Service { /** - * Creates a new Service. + * Creates a new {@link Service}. * - * @param {GPT} gt The containing GPT instance. + * @param {GPT} gpt The containing {@link GPT} instance. * @param {string} name The name of the service */ - constructor(gt, name) { + constructor(gpt, name) { this._name = name; - this._gt = gt; + this._gpt = gpt; this._enabled = false; this._used = false; this._listeners = {}; @@ -21,8 +21,9 @@ export default class Service { } /** - * UNDOCUMENTED - Returns the name of the service. + * Returns the name of the service. * + * @experimental * @returns {string} The name of the service */ getName() { @@ -30,8 +31,9 @@ export default class Service { } /** - * UNDOCUMENTED - Returns a version string. + * Returns a version string. * + * @experimental * @returns {string} The version string. */ getVersion() { @@ -39,17 +41,25 @@ export default class Service { } /** - * UNDOCUMENTED - Returns the slots with this service enabled. + * Returns the slots with this service enabled. * + * @experimental * @returns {Array} The slots */ getSlots() { return this._slots.slice(0); } + /** + * Adds the given slot to this service. + * + * @param {Slot} slot The slot to add + * @returns {Slot} + * @private + */ _addSlot(slot) { if (this._slots.indexOf(slot) === -1) { - this._gt._addSlot(slot); + this._gpt._addSlot(slot); this._slots.push(slot); this._slotCounter = this._slotCounter + 1; @@ -62,20 +72,27 @@ export default class Service { return slot; } + /** + * Removes the given slot from the service. + * + * @param {Slot} slot The slot to remove + * @private + */ _removeSlot(slot) { const index = this._slots.indexOf(slot); if (index !== -1) { this._slots.splice(index, 1); - this._gt.removeSlot(slot); - const id = `${slot.getAdUnitPath()}_${index}`; + this._gpt._removeSlot(slot); + const id = `${slot.getAdUnitPath()}_${index + 1}`; delete this._slotIdMap[id]; } } /** - * UNDOCUMENTED - Returns a map of ID's to slots. + * Returns a map of IDs to slots. * - * @returns {Object} The map of ID's to slots. + * @experimental + * @returns {Object} The map of IDs to slots. */ getSlotIdMap() { return Object.assign({}, this._slotIdMap); @@ -83,7 +100,7 @@ export default class Service { /** * Registers a listener that allows you to set up and call a JavaScript - * function when a specific GPT event happens on the page. The following + * function when a specific {@link GPT} event happens on the page. The following * events are supported: * * googletag.events.ImpressionViewableEvent * * googletag.events.SlotOnloadEvent @@ -94,7 +111,7 @@ export default class Service { * * @param {string} eventType A string representing the type of event generated * by GPT. Event types are case sensitive. - * @param {function(Object)} listener Function that takes a single event object argument. + * @param {function(event: Object)} listener Function that takes a single event object argument. * @returns {Service} The service object on which the method was called. */ addEventListener(eventType, listener) { @@ -103,8 +120,16 @@ export default class Service { return this; } - _fireEvent(event) { - for (let listener of (this._listeners[event.name] || [])) { + /** + * Fires the specified eventType, calling all registered listeners with the + * specified event object. + * + * @param {string} eventType The event type to fire. + * @param {Object} event The event object to pass to listeners. + * @private + */ + _fireEvent(eventType, event) { + for (let listener of (this._listeners[eventType] || [])) { listener(event); } } @@ -139,7 +164,7 @@ export default class Service { * * @param {string} key The name of the attribute. * @param {string} value Attribute value. - * @returns {*} The service object on which the method was called. + * @returns {Service} The service object on which the method was called. */ set(key, value) { this._attributes[key] = value; @@ -152,19 +177,21 @@ export default class Service { * * @param {string} adUnitPath Ad unit path of slot to be rendered. * @param {GeneralSize} size Width and height of the slot. - * @param {string=} optDiv ID of the div containing the slot. - * @param {string=} optClickUrl The click URL to use on this slot. + * @param {string} [optDiv] ID of the div containing the slot. + * @param {string} [optClickUrl] The click URL to use on this slot. */ display(adUnitPath, size, optDiv, optClickUrl) { this._used = true; this.enable(); - this._gt.defineSlot(adUnitPath, size, optDiv).addService(this).setClickUrl(optClickUrl); + this._gpt.defineSlot(adUnitPath, size, optDiv).addService(this).setClickUrl(optClickUrl); } + /** + * Enables the service. + * + * @experimental + */ enable() { this._enabled = true; - this._onEnable(); } - - _onEnable() {} } diff --git a/src/SingleSize.js b/src/SingleSize.js index 63d78ba..5871aa1 100644 --- a/src/SingleSize.js +++ b/src/SingleSize.js @@ -1,5 +1,6 @@ /** * @typedef {SingleSizeArray|NamedSize} SingleSize + * @private */ import * as SingleSizeArray from './SingleSizeArray'; import * as NamedSize from './NamedSize'; @@ -7,22 +8,24 @@ import * as NamedSize from './NamedSize'; /** * Returns true if the given object is a {@link SingleSize}. * + * @private * @param {SingleSize|*} obj The object to test */ export function isSingleSize(obj) { - return SingleSizeArray.isSingleSizeArray(obj) || NamedSize.isNamedSize(obj); + return obj != null && (SingleSizeArray.isSingleSizeArray(obj) || NamedSize.isNamedSize(obj)); } /** * Returns a {@link Size} instance from the {@link SingleSize}. * + * @private * @param {SingleSize|*} obj The object to convert. - * @returns {?Size} The Size instance or null if the object is not a {@link SingleSizeArray}. + * @returns {?Size} The {@link Size} instance or null if the object is not a {@link SingleSizeArray}. */ export function toSize(obj) { if (SingleSizeArray.isSingleSizeArray(obj)) { return SingleSizeArray.toSize(obj); } else { - return null; // TODO - what really happens here? + return null; } } diff --git a/src/SingleSizeArray.js b/src/SingleSizeArray.js index e07ca16..1bee0a7 100644 --- a/src/SingleSizeArray.js +++ b/src/SingleSizeArray.js @@ -1,16 +1,18 @@ /** * Array of two numbers representing [width, height]. * @typedef {Array} SingleSizeArray + * @private */ import Size from './Size'; /** * Returns true if the given object is a {@link SingleSizeArray}. * + * @private * @param {SingleSizeArray|*} obj The object to test */ export function isSingleSizeArray(obj) { - if (!Array.isArray(obj) || obj.length !== 2) { + if (obj == null || !Array.isArray(obj) || obj.length !== 2) { return false; } @@ -21,8 +23,10 @@ export function isSingleSizeArray(obj) { /** * Returns a {@link Size} instance from the {@link SingleSizeArray}. * + * @private * @param {SingleSizeArray|*} obj The object to convert. - * @returns {?Size} The Size instance or null if the object is not a {@link SingleSizeArray}. + * @returns {?Size} The {@link Size} instance or null if the object is not + * a {@link SingleSizeArray}. */ export function toSize(obj) { if (isSingleSizeArray(obj)) { @@ -33,6 +37,13 @@ export function toSize(obj) { } } +/** + * Returns true if the specified parameter is an integer. + * + * @private + * @param {*} n The value to check + * @returns {boolean} True if the value is an integer. + */ function isInteger(n) { return (typeof n === 'number') && (n % 1 === 0); } diff --git a/src/Size.js b/src/Size.js index 489ed7b..678ea96 100644 --- a/src/Size.js +++ b/src/Size.js @@ -1,32 +1,90 @@ /** - * Size is used internally in GPT and is not actually documented. + * Size captures a two dimensional size. + * + * @experimental */ export default class Size { /** - * Creates a new Size instance. - * @param {number} w The width - * @param {number} h The height + * Creates a new {@link Size} instance. + * + * @param {number} width The width + * @param {number} height The height */ - constructor(w, h) { - this.w = w; - this.h = h; + constructor(width, height) { + /** + * The width + * @type {number} + */ + this.width = width; + + /** + * The height + * @type {number} + */ + this.height = height; } /** * Gets the width in pixels. * - * @returns {number} The width in pixels + * @type {number} The width in pixels */ getWidth() { - return this.w; + return this.width; } /** * Gets the height in pixels. * - * @returns {number} The height in pixels + * @type {number} The height in pixels */ getHeight() { - return this.h; + return this.height; + } + + /** + * Returns whether the {@link Size} is empty. + * + * @experimental + * @type {boolean} true if the size is empty. + */ + isEmpty() { + return !(this.width * this.height); + } + + /** + * Truncates the width and height upward. + * + * @experimental + * @type {Size} The instance called + */ + ceil() { + this.width = Math.ceil(this.width); + this.height = Math.ceil(this.height); + return this; + } + + /** + * Truncates the width and height downard. + * + * @experimental + * @type {Size} The instance called + */ + floor() { + this.width = Math.floor(this.width); + this.height = Math.floor(this.height); + return this; + } + + /** + * Rounds the width and height. + * + * @experimental + * @type {Size} The instance called + */ + round() { + this.width = Math.round(this.width); + this.height = Math.round(this.height); + return this; } } diff --git a/src/SizeMapping.js b/src/SizeMapping.js index be8ec91..27a075b 100644 --- a/src/SizeMapping.js +++ b/src/SizeMapping.js @@ -1,7 +1,9 @@ /** - * Each size mapping is an array of two elements: SingleSizeArray and GeneralSize. + * Each size mapping is an array of two elements: {@link SingleSizeArray} and + * {@link GeneralSize}. * * @typedef {Array} SizeMapping + * @private */ import * as GeneralSize from './GeneralSize'; import * as SingleSizeArray from './SingleSizeArray'; @@ -9,6 +11,7 @@ import * as SingleSizeArray from './SingleSizeArray'; /** * Returns true if the object is a {@link SizeMapping}. * + * @private * @param {SizeMapping|*} obj The object to test */ export function isSizeMapping(obj) { diff --git a/src/SizeMappingArray.js b/src/SizeMappingArray.js index 81d6d63..bb11e14 100644 --- a/src/SizeMappingArray.js +++ b/src/SizeMappingArray.js @@ -1,11 +1,13 @@ /** * @typedef {Array} SizeMappingArray + * @private */ import * as SizeMapping from './SizeMapping'; /** * Returns true if the object is a {@link SizeMappingArray}. * + * @private * @param {SizeMappingArray|*} obj The object to test */ export function isSizeMappingArray(obj) { diff --git a/src/SizeMappingBuilder.js b/src/SizeMappingBuilder.js index 3a10d8e..9b87390 100644 --- a/src/SizeMappingBuilder.js +++ b/src/SizeMappingBuilder.js @@ -6,15 +6,15 @@ import * as SizeMappingArray from './SizeMappingArray'; */ export default class SizeMappingBuilder { /** - * Creates a new SizeMappingBuilder. + * Creates a new {@link SizeMappingBuilder}. */ constructor() { this._mappings = []; } /** - * Adds a mapping from a single-size array representing the viewport to either - * a single-size array or a multi-size array representing the slot. + * Adds a mapping from a {@link SingleSizeArray} representing the viewport to + * {@link GeneralSize} representing the slot. * * @param {SingleSizeArray} viewportSize The size of the viewport for this mapping entry. * @param {GeneralSize} slotSize The sizes of the slot for this mapping entry. @@ -30,7 +30,7 @@ export default class SizeMappingBuilder { * any invalid mappings have been supplied, this method will return null. * Otherwise it returns a specification in the correct format to pass to * {@link Slot#defineSizeMapping()}. The behavior of the builder after - * calling {@link build()} is undefined. + * calling {@link SizeMappingBuilder#build} is undefined. * * @returns {?SizeMappingArray} The result built by this builder. Can be null * if invalid size mappings were supplied. diff --git a/src/Slot.js b/src/Slot.js index 0d8813f..ac4ef9b 100644 --- a/src/Slot.js +++ b/src/Slot.js @@ -4,27 +4,29 @@ import ImpressionViewableEvent from './events/ImpressionViewableEvent'; import SlotOnloadEvent from './events/SlotOnloadEvent'; import SlotRenderEndedEvent from './events/SlotRenderEndedEvent'; import SlotVisibilityChangedEvent from './events/SlotVisibilityChangedEvent'; +import SlotId from './SlotId'; +import TargetingMap from './TargetingMap'; /** * Slot is an object representing single ad slot on a page. */ export default class Slot { /** - * Creates a new Slot. + * Creates a new {@link Slot}. * * @param {string} adUnitPath Full path of the ad unit with the network code and unit code. - * @param {GeneralSize=} size Width and height of the added slot. This is the + * @param {GeneralSize} [size] Width and height of the added slot. This is the * size that is used in the ad request if no responsive size mapping is provided * or the size of the viewport is smaller than the smallest size provided in the mapping. - * @param {string=} optDiv ID of the div that will contain this ad unit. + * @param {string} [optDiv] ID of the div that will contain this ad unit. + * @param {number} [instance = 0] The instance count of the slot. */ - constructor(adUnitPath, size, optDiv) { - this._adUnitPath = adUnitPath; - this._slotElementId = optDiv; + constructor(adUnitPath, size, optDiv, instance = 0) { + this._id = new SlotId(adUnitPath, instance, optDiv); this._sizes = GeneralSize.toSizes(size); this._services = []; this._categoryExclusions = []; - this._targeting = {}; + this._targeting = new TargetingMap(); this._attributes = {}; this._clickUrl = null; this._responseInformation = null; @@ -43,27 +45,48 @@ export default class Slot { }; } + /** + * Returns the name of the slot + * + * @experimental + * @returns {string} Ad unit path. + */ + getName() { + return this._id.getName(); + } + + /** + * Returns the instance of the {@link Slot}. + * + * @experimental + * @returns {number} The instance + */ + getDefinedId() { + return this._id.getInstance(); + } + /** * Returns the full path of the ad unit, with the network code and ad unit path. * * @returns {string} Ad unit path. */ getAdUnitPath() { - return this._adUnitPath; + return this._id.getAdUnitPath(); } /** - * Returns the id of the slot element provided when the slot was defined. + * Returns the id of the slot element provided when the {@link Slot} was defined. * * @returns {string} Slot element id. */ getSlotElementId() { - return this._slotElementId; + return this._id.getDomId(); } /** - * UNDOCUMENTED - returns the enabled services. + * Returns the enabled services. * + * @experimental * @returns {Array} The list of enabled services. */ getServices() { @@ -71,8 +94,9 @@ export default class Slot { } /** - * UNDOCUMENTED - returns the sizes. + * Returns the sizes. * + * @experimental * @returns {Array} The list of sizes. */ getSizes() { @@ -80,8 +104,9 @@ export default class Slot { } /** - * UNDOCUMENTED - returns a flag indicating whether the slot is out of the page. + * Returns a flag indicating whether the {@link Slot} is out of the page. * + * @experimental * @returns {boolean} */ getOutOfPage() { @@ -92,25 +117,21 @@ export default class Slot { * Initiates rendering of this slot. */ display() { - this._options.fetched = true; - this._options.displayed = true; - const isEmpty = this._options.content == null; - const lineItemId = this._responseInformation != null ? this._responseInformation.lineItemId : null; - const creativeId = this._responseInformation != null ? this._responseInformation.creativeId : null; - const size = this.getSizes()[0]; - - for (let service of this._services) { - service._fireEvent(new ImpressionViewableEvent(service.getName(), this)); - service._fireEvent(new SlotOnloadEvent(service.getName(), this)); - service._fireEvent(new SlotRenderEndedEvent(service.getName(), this, - creativeId, lineItemId, isEmpty, size)); - service._fireEvent(new SlotVisibilityChangedEvent(service.getName(), this, 100)); + if (!this._options.displayed) { + this.fetchStarted(); + this.fetchEnded(); + this.loaded(); + this.renderStarted(); + this.renderEnded(); + + this.impressionViewable(); + this.visibilityChanged(100); } } /** * Returns the list of attribute keys set on this slot. If you intend to see - * the keys of service-level attributes inherited by this slot, you have to + * the keys of service-level attributes inherited by this {@link Slot}, you have to * use the {@link PubAdsService#getAttributeKeys} API. * * @returns {!Array} Array of attribute keys. Ordering is undefined. @@ -141,13 +162,19 @@ export default class Slot { * * @param {string} key The name of the attribute. * @param {string} value Attribute value. - * @returns {Slot} The slot object on which the method was called. + * @returns {Slot} The {@link Slot} object on which the method was called. */ set(key, value) { this._attributes[key] = value; return this; } + /** + * Return the attribute map. + * + * @private + * @returns {Object} The attribute map + */ _getAttributes() { return Object.assign({}, this._attributes); } @@ -161,7 +188,22 @@ export default class Slot { * array if there is no such key. */ getTargeting(key) { - return this._targeting[key] || []; + return this._targeting.get(key); + } + + /** + * Sets a custom targeting parameter for this slot. Calling this method + * multiple times for the same key will overwrite old values. Values set here + * will overwrite targeting parameters set on service level. These keys are + * defined in your {@link DFP} account. + * + * @param {string} key Targeting parameter key. + * @param {string|!Array} value Targeting parameter value or array of values. + * @returns {Slot} The {@link Slot} object on which the method was called. + */ + setTargeting(key, value) { + this._targeting.set(key, value); + return this; } /** @@ -171,38 +213,39 @@ export default class Slot { * @returns {!Array} Array of targeting keys. Ordering is undefined. */ getTargetingKeys() { - return Object.keys(this._targeting); + return this._targeting.keys(); } /** - * Sets a custom targeting parameter for this slot. Calling this method - * multiple times for the same key will overwrite old values. Values set here - * will overwrite targeting parameters set on service level. These keys are - * defined in your DFP account. + * Returns the current targeting key-value dictionary. * - * @param {string} key Targeting parameter key. - * @param {string|!Array} value Targeting parameter value or array of values. - * @returns {Slot} The slot object on which the method was called. + * @experimental + * @returns {Object>} the targeting map. */ - setTargeting(key, value) { - if (Array.isArray(value)) { - this._targeting[key] = value; - } else { - this._targeting[key] = [value]; - } + getTargetingMap() { + return this._targeting.all(); + } + + /** + * Clears all custom slot-level targeting parameters for this slot. + * + * @returns {Slot} The {@link Slot} object on which the method was called. + */ + clearTargeting() { + this._targeting.clear(); return this; } /** * Sets custom targeting parameters for this passback slot, from a key:value * map in a JSON object. A badly formatted input will be rejected. This is the - * same as calling @{link #setTargeting} for all the key values + * same as calling @{link Slot#setTargeting} for all the key values * of the object. In case of overwriting, only the last value will be kept. * Values set here will overwrite targeting parameters set on service level. - * These keys are defined in your DFP account. + * These keys are defined in your {@link DFP} account. * * @param {Object>} map Targeting parameter key:value map. - * @returns {Slot} The slot object on which the method was called. + * @returns {Slot} The {@link Slot} object on which the method was called. */ updateTargetingFromMap(map) { for (let key in map) { @@ -213,25 +256,11 @@ export default class Slot { return this; } - /** - * Clears all custom slot-level targeting parameters for this slot. - * - * @returns {Slot} The slot object on which the method was called. - */ - clearTargeting() { - this._targeting = {}; - return this; - } - - _getTargetingMap() { - return Object.assign({}, this._targeting); - } - /** * Adds a service to this slot. * * @param {Service} service The service to be added. - * @returns {Slot} The slot object on which the method was called. + * @returns {Slot} The {@link Slot} object on which the method was called. */ addService(service) { if (this._services.indexOf(service) === -1) { @@ -241,6 +270,11 @@ export default class Slot { return this; } + /** + * Removes all of the services from the slot. + * + * @private + */ _removeServices() { for (let service of this._services) { service._removeSlot(this); @@ -249,10 +283,6 @@ export default class Slot { this._services = []; } - _hasService(service) { - return this._services.indexOf(service) !== -1; - } - /** * Returns the ad category exclusion labels for this slot. * @@ -266,7 +296,7 @@ export default class Slot { * Sets a slot-level ad category exclusion label on this slot. * * @param {string} categoryExclusion The ad category exclusion label to add. - * @returns {Slot} The slot object on which the method was called. + * @returns {Slot} The {@link Slot} object on which the method was called. */ setCategoryExclusion(categoryExclusion) { this._categoryExclusions.push(categoryExclusion); @@ -276,7 +306,7 @@ export default class Slot { /** * Clears all slot-level ad category exclusion labels for this slot. * - * @returns {Slot} The slot object on which the method was called. + * @returns {Slot} The {@link Slot} object on which the method was called. */ clearCategoryExclusions() { this._categoryExclusions = []; @@ -287,30 +317,24 @@ export default class Slot { * Sets an array of mappings from a minimum viewport size to slot size for this slot. * * @param {!SizeMappingArray} sizeMapping Array of size mappings. You can use - * SizeMappingBuilder to create it. Each size mapping is an array of two elements: - * SingleSizeArray and GeneralSize. - * @returns {Slot} The slot object on which the method was called. + * {@link SizeMappingBuilder} to create it. Each size mapping is an array of two elements: + * {@link SingleSizeArray} and {@link GeneralSize}. + * @returns {Slot} The {@link Slot} object on which the method was called. + * @throws {Error} Size mapping has to be an array */ defineSizeMapping(sizeMapping) { if (!SizeMappingArray.isSizeMappingArray(sizeMapping)) { - // TODO - throw error + throw Error('Size mapping has to be an array'); } this._sizeMapping = sizeMapping; - return this; - } - _getSizeMapping() { - if (this._sizeMapping != null) { - return this._sizeMapping.slice(0); - } else { - return null; - } + return this; } /** * Sets the click URL to which users will be redirected after clicking on the - * ad. The DFP servers still record a click even if the click URL is replaced, + * ad. The {@link DFP} servers still record a click even if the click URL is replaced, * but any landing page URL associated with the creative that is served is * overridden. Subsequent calls overwrite the value. This works only for * non-SRA requests @@ -324,8 +348,9 @@ export default class Slot { } /** - * UNDOCUMENTED - gets the click URL. + * Gets the click URL. * + * @experimental * @returns {?string} The click URL */ getClickUrl() { @@ -343,19 +368,14 @@ export default class Slot { return this._responseInformation; } - _setResponseInformation(responseInformation) { - this._responseInformation = responseInformation; - return this; - } - /** * Sets whether the slot div should be hidden when there is no ad in the slot. * This overrides the service-level settings. * * @param {boolean} collapse Whether to collapse the slot if no ad is returned. - * @param {boolean=} optCollapseBeforeAdFetch Whether to collapse the slot + * @param {boolean} [optCollapseBeforeAdFetch] Whether to collapse the slot * even before an ad is fetched. Ignored if collapse is not true. - * @returns {Slot} The slot object on which the method was called. + * @returns {Slot} The {@link Slot} object on which the method was called. */ setCollapseEmptyDiv(collapse, optCollapseBeforeAdFetch) { this._options.collapseEmptyDiv = collapse; @@ -365,6 +385,16 @@ export default class Slot { return this; } + /** + * Returns whether the slot div should be hiddne when there is no ad in the slot. + * + * @experimental + * @returns {boolean} true to collapse the slot if no ad is returned. + */ + getCollapseEmptyDiv() { + return this._options.collapseEmptyDiv; + } + /** * Configures whether ads in this slot should be forced to be rendered using * a SafeFrame container @@ -373,7 +403,7 @@ export default class Slot { * rendered in SafeFrames and false to opt-out of a page-level setting (if * present). Setting this to false when not specified at page-level, won't * change anything. - * @returns {Slot} The slot object on which the method was called. + * @returns {Slot} The {@link Slot} object on which the method was called. */ setForceSafeFrame(forceSafeFrame) { this._options.forceSafeFrame = forceSafeFrame; @@ -387,27 +417,153 @@ export default class Slot { * preferences, if specified, will override any page level preferences. * * @param {SafeFrameConfig} config The configuration object. - * @returns {Slot} The slot object on which the method was called. + * @returns {Slot} The {@link Slot} object on which the method was called. */ setSafeFrameConfig(config) { this._options.safeFrameConfig = config; return this; } + /** + * Informs the {@link Slot} that its fetch has started. + * + * @experimental + */ + fetchStarted() { + this._options.fetched = false; + } + + /** + * Informs the {@link Slot} that its fetch has finished. + * + * @experimental + */ + fetchEnded() { + this._options.fetched = true; + } + + /** + * Informs the {@link Slot} that it has been loaded. + * + * @experimental + * @emits {SlotOnloadEvent} + */ + loaded() { + for (let service of this._services) { + const event = new SlotOnloadEvent(service.getName(), this); + service._fireEvent(event._name, event); + } + } + + /** + * Informs the {@link Slot} that its rendering has started. + * + * @experimental + */ + renderStarted() { + this._options.displayed = false; + } + + /** + * Informs the {@link Slot} that its rendering has finished. + * + * @experimental + * @param {?ResponseInformation} [responseInformation] The response information. + * @emits {SlotRenderEndedEvent} + */ + renderEnded(responseInformation) { + this._responseInformation = responseInformation; + this._options.displayed = true; + + const size = this.getSizes()[0]; + + for (let service of this._services) { + let event; + + if (responseInformation != null) { + event = new SlotRenderEndedEvent(service.getName(), this, + responseInformation.creativeId, responseInformation.lineItemId, false, size); + } else { + event = new SlotRenderEndedEvent(service.getName(), this, null, null, true, size); + } + + service._fireEvent(event._name, event); + } + } + + /** + * Notifies the {@link Slot} that it's impression is viewable. + * + * @emits {ImpressionViewableEvent} + */ + impressionViewable() { + for (let service of this._services) { + const event = new ImpressionViewableEvent(service.getName(), this); + + service._fireEvent(event._name, event); + } + } + + /** + * Notifies the {@link Slot} of its in view percentage. + * + * @param {number} inViewPercentage The percentage (0-100) of the ad's area that is visible. + * @emits {SlotVisibilityChangedEvent} + */ + visibilityChanged(inViewPercentage) { + if (this._options.inViewPercentage !== inViewPercentage) { + this._options.inViewPercentage = inViewPercentage; + for (let service of this._services) { + const event = new SlotVisibilityChangedEvent(service.getName(), this, inViewPercentage); + service._fireEvent(event._name, event); + } + } + } + + /** + * Refreshes the {@link Slot}. + * + * @private + */ _refresh() { this._options.refreshed += 1; - this._options.fetched = true; + + this.fetchStarted(); + this.fetchEnded(); + this.loaded(); + this.renderStarted(); + this.renderEnded(); + + this.impressionViewable(); + this.visibilityChanged(100); } + /** + * Clears the {@link Slot}. + * + * @private + */ _clear() { this._options.content = null; - this._options.fetched = null; + this._options.fetched = false; } + /** + * Sets the content of the {@link Slot}. + * + * @private + * @param {?string} content The content to set; or null to clear the content. + */ _setContent(content) { this._options.content = content; } + /** + * Gets the content of the slot {@link Slot}. + * + * @private + * @returns {?string} The content of the slot; or null if the slot has no content. + */ _getContent() { return this._options.content; } diff --git a/src/SlotId.js b/src/SlotId.js new file mode 100644 index 0000000..b6808a1 --- /dev/null +++ b/src/SlotId.js @@ -0,0 +1,74 @@ +/** + * The SlotId collects several identifiers for a Slot. + * + * @experimental + */ +export default class SlotId { + /** + * Creates a new {@link SlotId} instance. + * + * @param {string} adUnitPath The ad unit path of the slot. + * @param {number} instance The instance number of the slot. + * @param {?string} [optDomId] The DOM ID of the slot. + */ + constructor(adUnitPath, instance, optDomId) { + this._adUnitPath = adUnitPath; + this._instance = instance; + this._domId = optDomId || `gpt_unit_${adUnitPath}_${instance}`; + } + + /** + * The name of the {@link Slot}. + * + * @returns {string} The name of the {@link Slot}. + */ + getName() { + return this._adUnitPath; + } + + /** + * The AdUnitPath of the {@link Slot}. + * + * @returns {string} The AdUnitPath of the {@link Slot}. + */ + getAdUnitPath() { + return this._adUnitPath; + } + + /** + * The DOM Id of the {@link Slot}. + * + * @returns {string} The DOM Id of the {@link Slot}. + */ + getDomId() { + return this._domId; + } + + /** + * The ID of the {@link Slot}. + * + * @returns {string} The ID of the {@link Slot}. + */ + getId() { + return `${this._adUnitPath}_${this._instance}`; + } + + /** + * The instance number of the {@link Slot}. + * + * @returns {number} The instance number of the {@link Slot}. + */ + getInstance() { + return this._instance; + } + + /** + * Converts the {@link SlotId} to a String. + * + * @override + * @returns {string} A String representation. + */ + toString() { + return this.getId(); + } +} diff --git a/src/TargetingMap.js b/src/TargetingMap.js new file mode 100644 index 0000000..63c2069 --- /dev/null +++ b/src/TargetingMap.js @@ -0,0 +1,69 @@ +/** + * Manages setting and retrieving targeting. + * + * @experimental + */ +export default class TargetingMap { + /** + * Creates a new {@link TargetingMap} instance. + */ + constructor() { + this._targeting = {}; + } + + /** + * Sets custom targeting parameters for a given key. + * + * @param {string} key Targeting parameter key. + * @param {string|!Array} value Targeting parameter value or array of values. + */ + set(key, value) { + if (Array.isArray(value)) { + this._targeting[key] = value; + } else { + this._targeting[key] = [value]; + } + } + + /** + * Returns a specific targeting parameter that has been set. + * + * @param {string} key The targeting key to look for. + * @returns {!Array} The values associated with this key, or an empty + * array if there is no such key. + */ + get(key) { + return this._targeting[key] || []; + } + + /** + * Returns the list of all targeting keys that have been set. + * + * @returns {!Array} Array of targeting keys. Ordering is undefined. + */ + keys() { + return Object.keys(this._targeting); + } + + /** + * Clears custom targeting parameters for a given key. + * + * @param {string} [key] Targeting parameter key. + */ + clear(key) { + if (key == null) { + this._targeting = {}; + } else { + delete this._targeting[key]; + } + } + + /** + * Returns the current targeting key-value dictionary. + * + * @returns {Object>} the targeting map. + */ + all() { + return Object.assign({}, this._targeting); + } +} diff --git a/src/events/ImpressionViewableEvent.js b/src/events/ImpressionViewableEvent.js index 38003e1..cb67544 100644 --- a/src/events/ImpressionViewableEvent.js +++ b/src/events/ImpressionViewableEvent.js @@ -14,14 +14,20 @@ export default class ImpressionViewableEvent { this._slot = slot; } - get name() { + /** + * Name of the event. + * + * @private + * @type {string} + */ + get _name() { return 'googletag.events.ImpressionViewableEvent'; } /** * Name of the service that rendered the slot. * - * @returns {string} + * @type {string} */ get serviceName() { return this._serviceName; @@ -30,7 +36,7 @@ export default class ImpressionViewableEvent { /** * The slot which contains the impression that became viewable. * - * @returns {!Slot} + * @type {!Slot} */ get slot() { return this._slot; diff --git a/src/events/SlotOnloadEvent.js b/src/events/SlotOnloadEvent.js index 921a79a..1a9177d 100644 --- a/src/events/SlotOnloadEvent.js +++ b/src/events/SlotOnloadEvent.js @@ -15,14 +15,20 @@ export default class SlotOnloadEvent { this._slot = slot; } - get name() { + /** + * Name of the event. + * + * @private + * @type {string} + */ + get _name() { return 'googletag.events.SlotOnloadEvent'; } /** * Name of the service that rendered the slot. * - * @returns {string} + * @type {string} */ get serviceName() { return this._serviceName; @@ -31,7 +37,7 @@ export default class SlotOnloadEvent { /** * The slot in which the creative was loaded. * - * @returns {!Slot} + * @type {!Slot} */ get slot() { return this._slot; diff --git a/src/events/SlotRenderEndedEvent.js b/src/events/SlotRenderEndedEvent.js index 560d25e..ca09f6f 100644 --- a/src/events/SlotRenderEndedEvent.js +++ b/src/events/SlotRenderEndedEvent.js @@ -27,14 +27,20 @@ export default class SlotRenderEndedEvent { this._size = size; } - get name() { + /** + * Name of the event. + * + * @private + * @type {string} + */ + get _name() { return 'googletag.events.SlotRenderEndedEvent'; } /** * Name of the service that rendered the slot. * - * @returns {string} + * @type {string} */ get serviceName() { return this._serviceName; @@ -43,7 +49,7 @@ export default class SlotRenderEndedEvent { /** * The slot in which the creative was rendered. * - * @returns {!Slot} + * @type {!Slot} */ get slot() { return this._slot; @@ -53,7 +59,7 @@ export default class SlotRenderEndedEvent { * Creative ID of the rendered ad. Value is null for empty slots, backfill ads * or creatives rendered by services other than pubads service. * - * @returns {?number} + * @type {?number} */ get creativeId() { return this._creativeId; @@ -62,7 +68,7 @@ export default class SlotRenderEndedEvent { /** * true if no ad was returned for the slot, false otherwise. * - * @returns {boolean} + * @type {boolean} */ get isEmpty() { return this._isEmpty; @@ -72,7 +78,7 @@ export default class SlotRenderEndedEvent { * Line item ID of the rendered ad. Value is null for empty slots, backfill * ads or creatives rendered by services other than pubads service. * - * @returns {?number} + * @type {?number} */ get lineItemId() { return this._lineItemId; @@ -82,7 +88,7 @@ export default class SlotRenderEndedEvent { * Indicates the pixel size of the rendered creative. Example: [728, 90]. * Value is null for empty ad slots. * - * @returns {SingleSize} + * @type {SingleSize} */ get size() { return this._size; diff --git a/src/events/SlotVisibilityChangedEvent.js b/src/events/SlotVisibilityChangedEvent.js index 2978b8c..3ed1aca 100644 --- a/src/events/SlotVisibilityChangedEvent.js +++ b/src/events/SlotVisibilityChangedEvent.js @@ -12,12 +12,45 @@ export default class SlotVisibilityChangedEvent { * @param {number} inViewPercentage The percentage (0-100) of the ad's area that is visible. */ constructor(serviceName, slot, inViewPercentage) { - this.serviceName = serviceName; - this.slot = slot; - this.inViewPercentage = inViewPercentage; + this._serviceName = serviceName; + this._slot = slot; + this._inViewPercentage = inViewPercentage; } - get name() { + /** + * Name of the event. + * + * @private + * @type {string} + */ + get _name() { return 'googletag.events.SlotVisibilityChangedEvent'; } + + /** + * Name of the service that rendered the slot. + * + * @type {string} + */ + get serviceName() { + return this._serviceName; + } + + /** + * The slot which contains the impression that became viewable. + * + * @type {!Slot} + */ + get slot() { + return this._slot; + } + + /** + * The percentage of the ad that is "in view". + * + * @type {number} + */ + get inViewPercentage() { + return this._inViewPercentage; + } } diff --git a/src/main.js b/src/main.js index 1edc2c4..7637a84 100644 --- a/src/main.js +++ b/src/main.js @@ -1,3 +1,9 @@ import GPT from './GPT.js'; +/** @external {DFP} https://www.google.com/dfp */ + +/** + * @external {Array#push} https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/push + */ + module.exports = GPT; diff --git a/test/bugs/issue1.spec.js b/test/bugs/issue1.spec.js new file mode 100644 index 0000000..811c714 --- /dev/null +++ b/test/bugs/issue1.spec.js @@ -0,0 +1,23 @@ +import GPT from '../../src/GPT'; + +/* + * When filing a bug issue, add a failing unit test like the one below. + * + * Note this particular test passes now. + */ +describe('bugs', () => { + // replace 1 with the issue number and 'how to file bug' with a short + // description of the problem + describe('issue 1: how to file bug', () => { + // replace 'should set apiReady when loaded' with a short description of + // the expected behaviour + it('should set apiReady when loaded', () => { + const gpt = new GPT(); + + // Replace this code with steps to reproduce the problem + expect(gpt.apiReady).to.be(undefined); + gpt._loaded(); + expect(gpt.apiReady).to.be(true); + }); + }); +}); diff --git a/test/unit/CommandArray.spec.js b/test/unit/CommandArray.spec.js index ba30db5..2c43759 100644 --- a/test/unit/CommandArray.spec.js +++ b/test/unit/CommandArray.spec.js @@ -1,6 +1,8 @@ import CommandArray from '../../src/CommandArray'; +/** @test {CommandArray} */ describe('CommandArray', () => { + /** @test {CommandArray#constructor} */ describe('#constructor', () => { it('constructs with empty array', () => { const cmd = new CommandArray([]); @@ -18,7 +20,32 @@ describe('CommandArray', () => { }); }); + /** @test {CommandArray#push} */ describe('#push', () => { + it('ignores null', () => { + const cmd = new CommandArray([]); + + const result = cmd.push(null); + expect(result).to.be(0); + }); + + it('ignores undefined', () => { + const cmd = new CommandArray([]); + + const result = cmd.push(undefined); + expect(result).to.be(0); + }); + + it('ignores non-functions', () => { + const cmd = new CommandArray([]); + + const result = cmd.push('something'); + expect(result).to.be(0); + + const result2 = cmd.push({}); + expect(result2).to.be(0); + }); + it('executes the function and updates the count', () => { const cmd = new CommandArray([]); const fn = sinon.spy(); diff --git a/test/unit/CompanionAdsService.spec.js b/test/unit/CompanionAdsService.spec.js index df9657d..88a50e3 100644 --- a/test/unit/CompanionAdsService.spec.js +++ b/test/unit/CompanionAdsService.spec.js @@ -1,40 +1,44 @@ import CompanionAdsService from '../../src/CompanionAdsService'; import GPT from '../../src/GPT'; +/** @test {CompanionAdsService} */ describe('CompanionAdsService', () => { - const gt = new GPT(); + const gpt = new GPT(); + /** @test {CompanionAdsService#constructor} */ describe('#constructor', () => { it('constructs', () => { - const service = new CompanionAdsService(gt); + const service = new CompanionAdsService(gpt); expect(service).to.be.ok(); - expect(service).to.have.property('_gt', gt); + expect(service).to.have.property('_gpt', gpt); }); }); + /** @test {CompanionAdsService#enableSyncLoading} */ describe('#enableSyncLoading', () => { it('sets syncLoading option', () => { - const service = new CompanionAdsService(gt); + const service = new CompanionAdsService(gpt); service.enableSyncLoading(); expect(service._options).to.have.property('syncLoading', true); }); it('returns undefined', () => { - const service = new CompanionAdsService(gt); - expect(service.enableSyncLoading()).to.not.be.ok(); + const service = new CompanionAdsService(gpt); + expect(service.enableSyncLoading()).to.be(undefined); }); }); + /** @test {CompanionAdsService#setRefreshUnfilledSlots} */ describe('#setRefreshUnfilledSlots', () => { it('sets refreshUnfilledSlots option', () => { - const service = new CompanionAdsService(gt); + const service = new CompanionAdsService(gpt); service.setRefreshUnfilledSlots(true); expect(service._options).to.have.property('refreshUnfilledSlots', true); }); it('returns undefined', () => { - const service = new CompanionAdsService(gt); - expect(service.setRefreshUnfilledSlots()).to.not.be.ok(); + const service = new CompanionAdsService(gpt); + expect(service.setRefreshUnfilledSlots()).to.be(undefined); }); }); }); diff --git a/test/unit/ContentService.spec.js b/test/unit/ContentService.spec.js index 78f2c10..253d07c 100644 --- a/test/unit/ContentService.spec.js +++ b/test/unit/ContentService.spec.js @@ -2,40 +2,43 @@ import ContentService from '../../src/ContentService'; import GPT from '../../src/GPT'; import Slot from '../../src/Slot'; +/** @test {ContentService} */ describe('ContentService', () => { - const gt = new GPT(); + const gpt = new GPT(); const adUnitPath = '/Test/123'; const size = [728, 90]; const content = 'TEST CONTENT'; const content2 = 'TEST CONTENT 2'; + /** @test {ContentService#constructor} */ describe('#constructor', () => { it('constructs', () => { - const service = new ContentService(gt); + const service = new ContentService(gpt); expect(service).to.be.ok(); - expect(service).to.have.property('_gt', gt); + expect(service).to.have.property('_gpt', gpt); expect(service).to.have.property('_storedContent'); expect(service._storedContent).to.be.an('array'); expect(service._storedContent).to.be.empty(); }); }); + /** @test {ContentService#setContent} */ describe('#setContent', () => { it('returns undefined', () => { - const service = new ContentService(gt); + const service = new ContentService(gpt); const slot = new Slot(adUnitPath, size).addService(service); expect(service.setContent(slot, content)).to.not.be.ok(); }); it('stores content if service is not enabled', () => { - const service = new ContentService(gt); + const service = new ContentService(gpt); const slot = new Slot(adUnitPath, size).addService(service); service.setContent(slot, content); expect(slot._getContent()).to.not.be(content); }); it('sets content immediately if service is enabled', () => { - const service = new ContentService(gt); + const service = new ContentService(gpt); const slot = new Slot(adUnitPath, size).addService(service); service.enable(); service.setContent(slot, content); @@ -43,7 +46,7 @@ describe('ContentService', () => { }); it('overwrites previous content', () => { - const service = new ContentService(gt); + const service = new ContentService(gpt); const slot = new Slot(adUnitPath, size).addService(service); service.enable(); service.setContent(slot, content); @@ -53,14 +56,15 @@ describe('ContentService', () => { }); }); + /** @test {ContentService#enable} */ describe('#enable', () => { it('returns undefined', () => { - const service = new ContentService(gt); + const service = new ContentService(gpt); expect(service.enable()).to.not.be.ok(); }); it('sets any stored content', () => { - const service = new ContentService(gt); + const service = new ContentService(gpt); const slot = new Slot(adUnitPath, size).addService(service); service.setContent(slot, content); expect(slot._getContent()).to.not.be(content); diff --git a/test/unit/GPT.spec.js b/test/unit/GPT.spec.js index aa2e06d..0442979 100644 --- a/test/unit/GPT.spec.js +++ b/test/unit/GPT.spec.js @@ -1,6 +1,13 @@ import GPT from '../../src/GPT'; +import Slot from '../../src/Slot'; +/** @test {GPT} */ describe('GPT', () => { + const adUnitPath = '/Test/12345'; + const size = [728, 90]; + const optDiv = 'div-1'; + + /** @test {GPT#constructor} */ describe('#constructor', () => { it('constructs', () => { const gpt = new GPT(); @@ -8,6 +15,7 @@ describe('GPT', () => { }); }); + /** @test {GPT#getVersion} */ describe('#getVersion', () => { it('returns current version', () => { const gpt = new GPT(); @@ -15,6 +23,7 @@ describe('GPT', () => { }); }); + /** @test {GPT#companionAds} */ describe('#companionAds', () => { it('returns the service', () => { const gpt = new GPT(); @@ -23,6 +32,7 @@ describe('GPT', () => { }); }); + /** @test {GPT#content} */ describe('#content', () => { it('returns the service', () => { const gpt = new GPT(); @@ -31,6 +41,7 @@ describe('GPT', () => { }); }); + /** @test {GPT#pubads} */ describe('#pubads', () => { it('returns the service', () => { const gpt = new GPT(); @@ -39,6 +50,7 @@ describe('GPT', () => { }); }); + /** @test {GPT#enableServices} */ describe('#enableServices', () => { it('returns undefined', () => { const gpt = new GPT(); @@ -52,6 +64,7 @@ describe('GPT', () => { }); }); + /** @test {GPT#sizeMapping} */ describe('#sizeMapping', () => { it('returns a new builder', () => { const gpt = new GPT(); @@ -63,11 +76,8 @@ describe('GPT', () => { }); }); + /** @test {GPT#defineSlot} */ describe('#defineSlot', () => { - const adUnitPath = '/Test/12345'; - const size = [728, 90]; - const optDiv = null; - it('returns a slot', () => { const gpt = new GPT(); const slot = gpt.defineSlot(adUnitPath, size, optDiv); @@ -86,10 +96,8 @@ describe('GPT', () => { }); }); + /** @test {GPT#defineOutOfPageSlot} */ describe('#defineOutOfPageSlot', () => { - const adUnitPath = '/Test/12345'; - const optDiv = null; - it('returns a new slot', () => { const gpt = new GPT(); const slot = gpt.defineOutOfPageSlot(adUnitPath, optDiv); @@ -109,10 +117,8 @@ describe('GPT', () => { }); }); + /** @test {GPT#destroySlots} */ describe('#destroySlots', () => { - const adUnitPath = '/Test/12345'; - const optDiv = null; - it('returns true if no slots', () => { const gpt = new GPT(); @@ -152,15 +158,39 @@ describe('GPT', () => { expect(gpt._slots).to.be.eql([slot1]); }); + + it('returns false if slot does not exist', () => { + const gpt = new GPT(); + + expect(gpt._slots).to.be.an('array'); + expect(gpt._slots).to.be.empty(); + const slot1 = gpt.defineOutOfPageSlot(adUnitPath, optDiv); + const slot2 = gpt.defineOutOfPageSlot(adUnitPath, optDiv); + expect(slot1).to.be.ok(); + expect(slot2).to.be.ok(); + expect(gpt._slots).to.have.length(2); + expect(gpt._slots).to.eql([slot1, slot2]); + + expect(gpt.destroySlots([new Slot(adUnitPath, [], optDiv)])).to.be(false); + }); }); + /** @test {GPT#display} */ describe('#display', () => { it('returns undefined', () => { const gpt = new GPT(); expect(gpt.display('div')).to.be(undefined); }); + + it('displays the slot', () => { + const gpt = new GPT(); + const slot = gpt.defineSlot(adUnitPath, [], optDiv).addService(gpt.pubads()); + gpt.display(optDiv); + expect(slot._options.displayed).to.be(true); + }); }); + /** @test {GPT#openConsole} */ describe('#openConsole', () => { it('returns undefined', () => { const gpt = new GPT(); @@ -168,6 +198,7 @@ describe('GPT', () => { }); }); + /** @test {GPT#disablePublisherConsole} */ describe('#disablePublisherConsole', () => { it('returns undefined', () => { const gpt = new GPT(); @@ -175,6 +206,7 @@ describe('GPT', () => { }); }); + /** @test {GPT#setAdIframeTitle} */ describe('#setAdIframeTitle', () => { const title = 'Advertisement'; it('returns undefined', () => { @@ -189,6 +221,7 @@ describe('GPT', () => { }); }); + /** @test {GPT#_loaded} */ describe('#_loaded', () => { it('returns undefined', () => { const gpt = new GPT(); diff --git a/test/unit/GeneralSize.spec.js b/test/unit/GeneralSize.spec.js new file mode 100644 index 0000000..bbaf2fe --- /dev/null +++ b/test/unit/GeneralSize.spec.js @@ -0,0 +1,70 @@ +import * as GeneralSize from '../../src/GeneralSize'; +import Size from '../../src/Size'; + +/** @test {GeneralSize} */ +describe('GeneralSize', () => { + const triplet = [728, 90, 1]; + const singleSize = [728, 90]; + const multiSize = [[728, 90], [970, 90]]; + + /** @test {GeneralSize#isGeneralSize} */ + describe('#isGeneralSize', () => { + it('returns false for null', () => { + expect(GeneralSize.isGeneralSize(null)).to.be(false); + }); + + it('returns false for undefined', () => { + expect(GeneralSize.isGeneralSize(undefined)).to.be(false); + }); + + it('returns true for empty array', () => { + expect(GeneralSize.isGeneralSize([])).to.be(true); + }); + + it('returns true for SingleSize', () => { + expect(GeneralSize.isGeneralSize(singleSize)).to.be(true); + }); + + it('returns true for MultiSize', () => { + expect(GeneralSize.isGeneralSize(multiSize)).to.be(true); + }); + }); + + /** @test {GeneralSize#toSizes} */ + describe('#toSizes', () => { + it('returns empty array for null', () => { + const result = GeneralSize.toSizes(null); + expect(result).to.be.an('array'); + expect(result).to.be.empty(); + }); + + it('returns empty array for empty array', () => { + const result = GeneralSize.toSizes([]); + expect(result).to.be.an('array'); + expect(result).to.be.empty(); + }); + + it('returns empty array for triplet', () => { + const result = GeneralSize.toSizes(triplet); + expect(result).to.be.an('array'); + expect(result).to.be.empty(); + }); + + it('returns array of sizes for SingleSize', () => { + const result = GeneralSize.toSizes(singleSize); + expect(result).to.be.an('array'); + expect(result).to.be.eql([ + new Size(728, 90) + ]); + }); + + it('returns array of sizes for MultiSize', () => { + const result = GeneralSize.toSizes(multiSize); + expect(result).to.be.an('array'); + expect(result).to.be.eql([ + new Size(728, 90), + new Size(970, 90) + ]); + }); + }); +}); diff --git a/test/unit/MultiSize.spec.js b/test/unit/MultiSize.spec.js new file mode 100644 index 0000000..3de8af5 --- /dev/null +++ b/test/unit/MultiSize.spec.js @@ -0,0 +1,61 @@ +import * as MultiSize from '../../src/MultiSize'; +import Size from '../../src/Size'; + +/** @test {MultiSize} */ +describe('MultiSize', () => { + const singleSize = [728, 90]; + const multiSize = [[728, 90], [970, 90]]; + + /** @test {MultiSize#isMultiSize} */ + describe('#isMultiSize', () => { + it('returns false for null', () => { + expect(MultiSize.isMultiSize(null)).to.be(false); + }); + + it('returns false for undefined', () => { + expect(MultiSize.isMultiSize(undefined)).to.be(false); + }); + + it('returns true for empty array', () => { + expect(MultiSize.isMultiSize([])).to.be(true); + }); + + it('returns false for SingleSize', () => { + expect(MultiSize.isMultiSize(singleSize)).to.be(false); + }); + + it('returns true for MultiSize', () => { + expect(MultiSize.isMultiSize(multiSize)).to.be(true); + }); + }); + + /** @test {MultiSize#toSizes} */ + describe('#toSizes', () => { + it('returns empty array for null', () => { + const result = MultiSize.toSizes(null); + expect(result).to.be.an('array'); + expect(result).to.be.empty(); + }); + + it('returns empty array for object', () => { + const result = MultiSize.toSizes({}); + expect(result).to.be.an('array'); + expect(result).to.be.empty(); + }); + + it('returns empty array for SingleSize', () => { + const result = MultiSize.toSizes(singleSize); + expect(result).to.be.an('array'); + expect(result).to.be.empty(); + }); + + it('returns array of sizes for MultiSize', () => { + const result = MultiSize.toSizes(multiSize); + expect(result).to.be.an('array'); + expect(result).to.be.eql([ + new Size(728, 90), + new Size(970, 90) + ]); + }); + }); +}); diff --git a/test/unit/NamedSize.spec.js b/test/unit/NamedSize.spec.js new file mode 100644 index 0000000..d7aee14 --- /dev/null +++ b/test/unit/NamedSize.spec.js @@ -0,0 +1,38 @@ +import * as NamedSize from '../../src/NamedSize'; + +/** @test {NamedSize} */ +describe('NamedSize', () => { + const singleSize = [728, 90]; + const multiSize = [[728, 90], [970, 90]]; + + /** @test {NamedSize#isNamedSize} */ + describe('#isNamedSize', () => { + it('returns false for null', () => { + expect(NamedSize.isNamedSize(null)).to.be(false); + }); + + it('returns false for undefined', () => { + expect(NamedSize.isNamedSize(undefined)).to.be(false); + }); + + it('returns false for empty array', () => { + expect(NamedSize.isNamedSize([])).to.be(false); + }); + + it('returns false for SingleSize', () => { + expect(NamedSize.isNamedSize(singleSize)).to.be(false); + }); + + it('returns false for MultiSize', () => { + expect(NamedSize.isNamedSize(multiSize)).to.be(false); + }); + + it('returns false for non-"fluid" string', () => { + expect(NamedSize.isNamedSize('overflow')).to.be(false); + }); + + it('returns true for "fluid" string', () => { + expect(NamedSize.isNamedSize('fluid')).to.be(true); + }); + }); +}); diff --git a/test/unit/PubAdsService.spec.js b/test/unit/PubAdsService.spec.js index fe18783..150b774 100644 --- a/test/unit/PubAdsService.spec.js +++ b/test/unit/PubAdsService.spec.js @@ -3,11 +3,13 @@ import SafeFrameConfig from '../../src/SafeFrameConfig'; import GPT from '../../src/GPT'; import Slot from '../../src/Slot'; +/** @test {PubAdsService} */ describe('PubAdsService', () => { const gt = new GPT(); const adUnitPath = '/Test/12345'; const size = [728, 90]; + /** @test {PubAdsService#constructor} */ describe('#constructor', () => { it('constructs', () => { const service = new PubAdsService(gt); @@ -15,6 +17,7 @@ describe('PubAdsService', () => { }); }); + /** @test {PubAdsService#enable} */ describe('#enable', () => { it('returns undefined', () => { const service = new PubAdsService(gt); @@ -30,6 +33,7 @@ describe('PubAdsService', () => { }); }); + /** @test {PubAdsService#refresh} */ describe('#refresh', () => { it('returns undefined', () => { const service = new PubAdsService(gt); @@ -84,6 +88,7 @@ describe('PubAdsService', () => { }); }); + /** @test {PubAdsService#clear} */ describe('#clear', () => { const content = 'TEST CONTENT'; @@ -135,6 +140,7 @@ describe('PubAdsService', () => { }); }); + /** @test {PubAdsService#defineOutOfPagePassback} */ describe('#defineOutOfPagePassback', () => { it('returns the null if no adUnitPath', () => { const service = new PubAdsService(gt); @@ -151,6 +157,7 @@ describe('PubAdsService', () => { }); }); + /** @test {PubAdsService#definePassback} */ describe('#definePassback', () => { it('returns the null if no adUnitPath', () => { const service = new PubAdsService(gt); @@ -175,6 +182,7 @@ describe('PubAdsService', () => { }); }); + /** @test {PubAdsService#disableInitialLoad} */ describe('#disableInitialLoad', () => { it('returns undefined', () => { const service = new PubAdsService(gt); @@ -189,6 +197,7 @@ describe('PubAdsService', () => { }); }); + /** @test {PubAdsService#enableAsyncRendering} */ describe('#enableAsyncRendering', () => { it('returns true if not enabled', () => { const service = new PubAdsService(gt); @@ -209,6 +218,7 @@ describe('PubAdsService', () => { }); }); + /** @test {PubAdsService#enableSingleRequest} */ describe('#enableSingleRequest', () => { it('returns true if not enabled', () => { const service = new PubAdsService(gt); @@ -229,6 +239,7 @@ describe('PubAdsService', () => { }); }); + /** @test {PubAdsService#enableSyncRendering} */ describe('#enableSyncRendering', () => { it('returns true if not enabled', () => { const service = new PubAdsService(gt); @@ -249,6 +260,7 @@ describe('PubAdsService', () => { }); }); + /** @test {PubAdsService#enableVideoAds} */ describe('#enableVideoAds', () => { it('returns undefined', () => { const service = new PubAdsService(gt); @@ -263,6 +275,7 @@ describe('PubAdsService', () => { }); }); + /** @test {PubAdsService#setTargeting} */ describe('#setTargeting', () => { it('returns the service', () => { const service = new PubAdsService(gt); @@ -282,6 +295,7 @@ describe('PubAdsService', () => { }); }); + /** @test {PubAdsService#getTargeting} */ describe('#getTargeting', () => { it('returns empty array if no value', () => { const service = new PubAdsService(gt); @@ -297,6 +311,7 @@ describe('PubAdsService', () => { }); }); + /** @test {PubAdsService#getTargetingKeys} */ describe('#getTargetingKeys', () => { it('returns empty array if no targeting', () => { const service = new PubAdsService(gt); @@ -312,6 +327,7 @@ describe('PubAdsService', () => { }); }); + /** @test {PubAdsService#clearTargeting} */ describe('#clearTargeting', () => { it('returns the service', () => { const service = new PubAdsService(gt); @@ -329,6 +345,7 @@ describe('PubAdsService', () => { }); }); + /** @test {PubAdsService#collapseEmptyDivs} */ describe('#collapseEmptyDivs', () => { it('returns true if not enabled', () => { const service = new PubAdsService(gt); @@ -356,6 +373,7 @@ describe('PubAdsService', () => { }); }); + /** @test {PubAdsService#setCategoryExclusion} */ describe('#setCategoryExclusion', () => { it('returns the service', () => { const service = new PubAdsService(gt); @@ -381,6 +399,7 @@ describe('PubAdsService', () => { }); }); + /** @test {PubAdsService#clearCategoryExclusions} */ describe('#clearCategoryExclusions', () => { it('returns the service', () => { const service = new PubAdsService(gt); @@ -400,6 +419,7 @@ describe('PubAdsService', () => { }); }); + /** @test {PubAdsService#setCentering} */ describe('#setCentering', () => { it('returns the service', () => { const service = new PubAdsService(gt); @@ -413,6 +433,7 @@ describe('PubAdsService', () => { }); }); + /** @test {PubAdsService#setCookieOptions} */ describe('#setCookieOptions', () => { it('returns the service', () => { const service = new PubAdsService(gt); @@ -427,6 +448,7 @@ describe('PubAdsService', () => { }); }); + /** @test {PubAdsService#setForceSafeFrame} */ describe('#setForceSafeFrame', () => { it('returns the service', () => { const service = new PubAdsService(gt); @@ -441,6 +463,7 @@ describe('PubAdsService', () => { }); }); + /** @test {PubAdsService#setLocation} */ describe('#setLocation', () => { it('returns the service', () => { const service = new PubAdsService(gt); @@ -481,6 +504,7 @@ describe('PubAdsService', () => { }); }); + /** @test {PubAdsService#setPublisherProvidedId} */ describe('#setPublisherProvidedId', () => { const ppid = 'ppid_1234567'; @@ -496,6 +520,7 @@ describe('PubAdsService', () => { }); }); + /** @test {PubAdsService#setSafeFrameConfig} */ describe('#setSafeFrameConfig', () => { it('returns the service', () => { const allowOverlayExpansion = true; @@ -517,6 +542,7 @@ describe('PubAdsService', () => { }); }); + /** @test {PubAdsService#setTagForChildDirectedTreatment} */ describe('#setTagForChildDirectedTreatment', () => { it('returns the service', () => { const service = new PubAdsService(gt); @@ -531,6 +557,7 @@ describe('PubAdsService', () => { }); }); + /** @test {PubAdsService#clearTagForChildDirectedTreatment} */ describe('#clearTagForChildDirectedTreatment', () => { it('returns the service', () => { const service = new PubAdsService(gt); @@ -547,6 +574,7 @@ describe('PubAdsService', () => { }); }); + /** @test {PubAdsService#setVideoContent} */ describe('#setVideoContent', () => { const videoContentId = 'CONTENTID'; const videoCmsId = 'CMSID'; @@ -563,6 +591,7 @@ describe('PubAdsService', () => { }); }); + /** @test {PubAdsService#updateCorrelator} */ describe('#updateCorrelator', () => { it('returns the service', () => { const service = new PubAdsService(gt); diff --git a/test/unit/ResponseInformation.spec.js b/test/unit/ResponseInformation.spec.js index 585334a..b30dd92 100644 --- a/test/unit/ResponseInformation.spec.js +++ b/test/unit/ResponseInformation.spec.js @@ -1,6 +1,8 @@ import ResponseInformation from '../../src/ResponseInformation'; +/** @test {ResponseInformation} */ describe('ResponseInformation', () => { + /** @test {ResponseInformation#constructor} */ describe('#constructor', () => { it('constructs and properties are readonly', () => { const advertiserId = 'adv'; diff --git a/test/unit/SafeFrameConfig.spec.js b/test/unit/SafeFrameConfig.spec.js index 39b4abf..146ac60 100644 --- a/test/unit/SafeFrameConfig.spec.js +++ b/test/unit/SafeFrameConfig.spec.js @@ -1,6 +1,8 @@ import SafeFrameConfig from '../../src/SafeFrameConfig'; +/** @test {SafeFrameConfig} */ describe('SafeFrameConfig', () => { + /** @test {SafeFrameConfig#constructor} */ describe('#constructor', () => { it('constructs and sets readonly properties', () => { const allowOverlayExpansion = true; diff --git a/test/unit/Service.spec.js b/test/unit/Service.spec.js index 1bc5a71..cf4181a 100644 --- a/test/unit/Service.spec.js +++ b/test/unit/Service.spec.js @@ -2,45 +2,50 @@ import Service from '../../src/Service'; import GPT from '../../src/GPT'; import Slot from '../../src/Slot'; +/** @test {Service} */ describe('Service', () => { - const gt = new GPT(); + const gpt = new GPT(); const name = 'something'; const adUnitPath = '/Test/123'; const size = [728, 90]; const eventType = 'some.event'; + /** @test {Service#constructor} */ describe('#constructor', () => { it('constructs', () => { - const service = new Service(gt, name); + const service = new Service(gpt, name); expect(service).to.be.ok(); - expect(service).to.have.property('_gt', gt); + expect(service).to.have.property('_gpt', gpt); expect(service).to.have.property('_name', name); }); }); + /** @test {Service#getName} */ describe('#getName', () => { it('returns the name', () => { - const service = new Service(gt, name); + const service = new Service(gpt, name); expect(service.getName()).to.be(name); }); }); + /** @test {Service#getVersion} */ describe('#getVersion', () => { it('returns "unversioned"', () => { - const service = new Service(gt, name); + const service = new Service(gpt, name); expect(service.getVersion()).to.be('unversioned'); }); }); + /** @test {Service#getSlots} */ describe('#getSlots', () => { it('returns empty array if no slots', () => { - const service = new Service(gt, name); + const service = new Service(gpt, name); expect(service.getSlots()).to.be.an('array'); expect(service.getSlots()).to.be.empty(); }); it('return array of slots where this service is enabled', () => { - const service = new Service(gt, name); + const service = new Service(gpt, name); const slot1 = new Slot(adUnitPath, size).addService(service); const slot2 = new Slot(adUnitPath, size).addService(service); expect(service.getSlots()).to.be.an('array'); @@ -48,14 +53,15 @@ describe('Service', () => { }); }); + /** @test {Service#getSlotIdMap} */ describe('#getSlotIdMap', () => { it('returns empty dictionary if no slots', () => { - const service = new Service(gt, name); + const service = new Service(gpt, name); expect(service.getSlotIdMap()).to.be.empty(); }); it('returns dictionary mapping keys to slots', () => { - const service = new Service(gt, name); + const service = new Service(gpt, name); const slot1 = new Slot(adUnitPath, size).addService(service); const slot2 = new Slot(adUnitPath, size).addService(service); expect(service.getSlotIdMap()).to.be.an('object'); @@ -64,17 +70,29 @@ describe('Service', () => { [`${adUnitPath}_2`]: slot2 }); }); + + it('returns dictionary without mapping keys for removed slots', () => { + const service = new Service(gpt, name); + const slot1 = new Slot(adUnitPath, size).addService(service); + const slot2 = new Slot(adUnitPath, size).addService(service); + expect(service.getSlotIdMap()).to.be.an('object'); + gpt.destroySlots([slot1]); + expect(service.getSlotIdMap()).to.be.eql({ + [`${adUnitPath}_2`]: slot2 + }); + }); }); + /** @test {Service#addEventListener} */ describe('#addEventListener', () => { it('returns the service', () => { - const service = new Service(gt, name); + const service = new Service(gpt, name); const fn = sinon.spy(); expect(service.addEventListener(eventType, fn)).to.be(service); }); it('adds the listener to event listener map', () => { - const service = new Service(gt, name); + const service = new Service(gpt, name); const fn = sinon.spy(); service.addEventListener(eventType, fn); expect(service).to.have.property('_listeners'); @@ -85,28 +103,30 @@ describe('Service', () => { }); }); + /** @test {Service#get} */ describe('#get', () => { it('returns empty map if no attribute', () => { - const service = new Service(gt, name); + const service = new Service(gpt, name); expect(service.get('attr1')).to.be(null); }); it('attribute value if set', () => { - const service = new Service(gt, name); + const service = new Service(gpt, name); service.set('attr1', 'value1'); expect(service.get('attr1')).to.be('value1'); }); }); + /** @test {Service#getAttributeKeys} */ describe('#getAttributeKeys', () => { it('returns empty array if no attributes', () => { - const service = new Service(gt, name); + const service = new Service(gpt, name); expect(service.getAttributeKeys()).to.be.an('array'); expect(service.getAttributeKeys()).to.be.empty(); }); it('returns array with with attribute keys', () => { - const service = new Service(gt, name); + const service = new Service(gpt, name); service.set('attr1', 'value1'); service.set('attr2', 'value2'); expect(service.getAttributeKeys()).to.be.an('array'); @@ -114,61 +134,69 @@ describe('Service', () => { }); }); + /** @test {Service#set} */ describe('#set', () => { it('returns the service', () => { - const service = new Service(gt, name); + const service = new Service(gpt, name); expect(service.set('attr1', 'value1')).to.be(service); }); it('save the attribute value', () => { - const service = new Service(gt, name); + const service = new Service(gpt, name); service.set('attr1', 'value1'); expect(service.get('attr1')).to.be('value1'); }); }); + /** @test {Service#display} */ describe('#display', () => { it('returns undefined', () => { - const service = new Service(gt, name); + const service = new Service(gpt, name); expect(service.display(adUnitPath, size)).to.be(undefined); }); it('marks the service as used', () => { - const service = new Service(gt, name); + const service = new Service(gpt, name); service.display(adUnitPath, size); expect(service._used).to.be(true); }); it('enables the service', () => { - const service = new Service(gt, name); + const service = new Service(gpt, name); service.display(adUnitPath, size); expect(service._enabled).to.be(true); }); it('adds a slot', () => { - const service = new Service(gt, name); + const service = new Service(gpt, name); service.display(adUnitPath, size); expect(service._slots).to.be.an('array'); expect(service._slots[0].getAdUnitPath()).to.be(adUnitPath); }); }); + /** @test {Service#enable} */ describe('#enable', () => { it('set enabled flag', () => { - const service = new Service(gt, name); + const service = new Service(gpt, name); service.enable(); expect(service._enabled).to.be(true); }); + }); - it('call _onEnable', sinon.test(function() { - const service = new Service(gt, name); - const mock = this.mock(service); - mock.expects('_onEnable').once(); - - new Slot(adUnitPath, size).addService(service); - service.enable(); - expect(service._enabled).to.be(true); - })); + /** @test {Service#_fireEvent} */ + describe('#_fireEvent', () => { + it('calls all listeners', () => { + const service = new Service(gpt, name); + const fn1 = sinon.spy(); + const fn2 = sinon.spy(); + const eventObject = {name: eventType}; + service.addEventListener(eventType, fn1); + service.addEventListener(eventType, fn2); + service._fireEvent(eventType, eventObject); + expect(fn1.calledWith(eventObject)).to.be(true); + expect(fn2.calledWith(eventObject)).to.be(true); + }); }); }); diff --git a/test/unit/SingleSize.spec.js b/test/unit/SingleSize.spec.js new file mode 100644 index 0000000..267275e --- /dev/null +++ b/test/unit/SingleSize.spec.js @@ -0,0 +1,70 @@ +import * as SingleSize from '../../src/SingleSize'; +import Size from '../../src/Size'; + +/** @test {SingleSize} */ +describe('SingleSize', () => { + const singleSize = [728, 90]; + const multiSize = [[728, 90], [970, 90]]; + + /** @test {SingleSize#isSingleSize} */ + describe('#isSingleSize', () => { + it('returns false for null', () => { + expect(SingleSize.isSingleSize(null)).to.be(false); + }); + + it('returns false for undefined', () => { + expect(SingleSize.isSingleSize(undefined)).to.be(false); + }); + + it('returns false for empty array', () => { + expect(SingleSize.isSingleSize([])).to.be(false); + }); + + it('returns true for SingleSize', () => { + expect(SingleSize.isSingleSize(singleSize)).to.be(true); + }); + + it('returns false for MultiSize', () => { + expect(SingleSize.isSingleSize(multiSize)).to.be(false); + }); + + it('returns false for non-"fluid" string', () => { + expect(SingleSize.isSingleSize('overflow')).to.be(false); + }); + + it('returns true for "fluid" string', () => { + expect(SingleSize.isSingleSize('fluid')).to.be(true); + }); + }); + + /** @test {SingleSize#toSize} */ + describe('#toSize', () => { + it('returns null for null', () => { + expect(SingleSize.toSize(null)).to.be(null); + }); + + it('returns null for undefined', () => { + expect(SingleSize.toSize(undefined)).to.be(null); + }); + + it('returns null for empty array', () => { + expect(SingleSize.toSize([])).to.be(null); + }); + + it('returns Size for SingleSize', () => { + expect(SingleSize.toSize(singleSize)).to.eql(new Size(728, 90)); + }); + + it('returns null for MultiSize', () => { + expect(SingleSize.toSize(multiSize)).to.be(null); + }); + + it('returns null for non-"fluid" string', () => { + expect(SingleSize.toSize('overflow')).to.be(null); + }); + + xit('returns UNKNOWN for "fluid" string', () => { + expect(SingleSize.toSize('fluid')).to.be(true); + }); + }); +}); diff --git a/test/unit/SingleSizeArray.spec.js b/test/unit/SingleSizeArray.spec.js new file mode 100644 index 0000000..ff5652a --- /dev/null +++ b/test/unit/SingleSizeArray.spec.js @@ -0,0 +1,88 @@ +import * as SingleSizeArray from '../../src/SingleSizeArray'; +import Size from '../../src/Size'; + +/** @test {SingleSizeArray} */ +describe('SingleSizeArray', () => { + const singleSize = [728, 90]; + const triplet = [728, 90, 200]; + const singlet = [728]; + const multiSize = [[728, 90], [970, 90]]; + + /** @test {SingleSizeArray#isSingleSize} */ + describe('#isSingleSize', () => { + it('returns false for null', () => { + expect(SingleSizeArray.isSingleSizeArray(null)).to.be(false); + }); + + it('returns false for undefined', () => { + expect(SingleSizeArray.isSingleSizeArray(undefined)).to.be(false); + }); + + it('returns false for empty array', () => { + expect(SingleSizeArray.isSingleSizeArray([])).to.be(false); + }); + + it('returns true for SingleSize', () => { + expect(SingleSizeArray.isSingleSizeArray(singleSize)).to.be(true); + }); + + it('returns false for a triple', () => { + expect(SingleSizeArray.isSingleSizeArray(triplet)).to.be(false); + }); + + it('returns false for a single', () => { + expect(SingleSizeArray.isSingleSizeArray(singlet)).to.be(false); + }); + + it('returns false for MultiSize', () => { + expect(SingleSizeArray.isSingleSizeArray(multiSize)).to.be(false); + }); + + it('returns false for non-"fluid" string', () => { + expect(SingleSizeArray.isSingleSizeArray('overflow')).to.be(false); + }); + + it('returns false for "fluid" string', () => { + expect(SingleSizeArray.isSingleSizeArray('fluid')).to.be(false); + }); + }); + + /** @test {SingleSizeArray#toSize} */ + describe('#toSize', () => { + it('returns null for null', () => { + expect(SingleSizeArray.toSize(null)).to.be(null); + }); + + it('returns null for undefined', () => { + expect(SingleSizeArray.toSize(undefined)).to.be(null); + }); + + it('returns null for empty array', () => { + expect(SingleSizeArray.toSize([])).to.be(null); + }); + + it('returns Size for SingleSize', () => { + expect(SingleSizeArray.toSize(singleSize)).to.eql(new Size(728, 90)); + }); + + it('returns null for a triple', () => { + expect(SingleSizeArray.toSize(triplet)).to.be(null); + }); + + it('returns null for a single', () => { + expect(SingleSizeArray.toSize(singlet)).to.be(null); + }); + + it('returns null for MultiSize', () => { + expect(SingleSizeArray.toSize(multiSize)).to.be(null); + }); + + it('returns null for non-"fluid" string', () => { + expect(SingleSizeArray.toSize('overflow')).to.be(null); + }); + + xit('returns null for "fluid" string', () => { + expect(SingleSizeArray.toSize('fluid')).to.be(null); + }); + }); +}); diff --git a/test/unit/Size.spec.js b/test/unit/Size.spec.js index 64e59d0..f26d6a6 100644 --- a/test/unit/Size.spec.js +++ b/test/unit/Size.spec.js @@ -1,15 +1,18 @@ import Size from '../../src/Size'; +/** @test {Size} */ describe('Size', () => { + /** @test {Size#constructor} */ describe('#constructor', () => { it('constructs', () => { const size = new Size(728, 90); expect(size).to.be.ok(); - expect(size).to.have.property('w', 728); - expect(size).to.have.property('h', 90); + expect(size).to.have.property('width', 728); + expect(size).to.have.property('height', 90); }); }); + /** @test {Size#getWidth} */ describe('#getWidth', () => { it('returns the width', () => { const size = new Size(300, 250); @@ -17,10 +20,53 @@ describe('Size', () => { }); }); + /** @test {Size#getHeight} */ describe('#getHeight', () => { it('returns the height', () => { const size = new Size(300, 600); expect(size.getHeight()).to.be(600); }); }); + + /** @test {Size#isEmpty} */ + describe('#isEmpty', () => { + it('returns false for a non-empty Size', () => { + const size = new Size(300, 600); + expect(size.isEmpty()).to.be(false); + }); + + it('returns true for an empty Size', () => { + const size = new Size(0, 0); + expect(size.isEmpty()).to.be(true); + }); + }); + + /** @test {Size#ceil} */ + describe('#ceil', () => { + it('truncates upward', () => { + const size = new Size(299.999999, 599.99999); + expect(size.ceil()).to.eql(new Size(300, 600)); + }); + }); + + /** @test {Size#floor} */ + describe('#floor', () => { + it('truncates downward', () => { + const size = new Size(300.000000001, 600.0000001); + expect(size.floor()).to.eql(new Size(300, 600)); + }); + }); + + /** @test {Size#round} */ + describe('#round', () => { + it('rounds the size downward', () => { + const size = new Size(300.000000001, 600.0000001); + expect(size.round()).to.eql(new Size(300, 600)); + }); + + it('rounds the size upward', () => { + const size = new Size(299.999999, 599.99999); + expect(size.round()).to.eql(new Size(300, 600)); + }); + }); }); diff --git a/test/unit/SizeMapping.spec.js b/test/unit/SizeMapping.spec.js new file mode 100644 index 0000000..12be401 --- /dev/null +++ b/test/unit/SizeMapping.spec.js @@ -0,0 +1,57 @@ +import * as SizeMapping from '../../src/SizeMapping'; + +/** @test {SizeMapping} */ +describe('SizeMapping', () => { + const singlet = [1600]; + const triplet = [1600, 1200, 1]; + const viewport = [1600, 1200]; + const singleMappings = [768, 60]; + const multiMappings = [[768, 60], [970, 60]]; + + /** @test {SizeMapping#isSizeMapping} */ + describe('#isSizeMapping', () => { + it('returns false for null', () => { + expect(SizeMapping.isSizeMapping(null)).to.be(false); + }); + + it('returns false for undefined', () => { + expect(SizeMapping.isSizeMapping(undefined)).to.be(false); + }); + + it('returns false for singlet', () => { + expect(SizeMapping.isSizeMapping(singlet)).to.be(false); + }); + + it('returns false for triplet', () => { + expect(SizeMapping.isSizeMapping(triplet)).to.be(false); + }); + + it('returns true for good single mapping', () => { + expect(SizeMapping.isSizeMapping([viewport, singleMappings])).to.be(true); + }); + + it('returns true for good multi mapping', () => { + expect(SizeMapping.isSizeMapping([viewport, multiMappings])).to.be(true); + }); + + it('returns true for good fluid mapping', () => { + expect(SizeMapping.isSizeMapping([viewport, 'fluid'])).to.be(true); + }); + + it('returns false for singlet mapping', () => { + expect(SizeMapping.isSizeMapping([viewport, singlet])).to.be(false); + }); + + it('returns false for triplet mapping', () => { + expect(SizeMapping.isSizeMapping([viewport, triplet])).to.be(false); + }); + + it('returns false for bad viewport', () => { + expect(SizeMapping.isSizeMapping(['fluid', singleMappings])).to.be(false); + }); + + it('returns false for bad viewport multi', () => { + expect(SizeMapping.isSizeMapping([multiMappings, singleMappings])).to.be(false); + }); + }); +}); diff --git a/test/unit/SizeMappingArray.spec.js b/test/unit/SizeMappingArray.spec.js new file mode 100644 index 0000000..16eb440 --- /dev/null +++ b/test/unit/SizeMappingArray.spec.js @@ -0,0 +1,39 @@ +import * as SizeMappingArray from '../../src/SizeMappingArray'; + +/** @test {SizeMappingArray} */ +describe('SizeMappingArray', () => { + const viewport1 = [1600, 1200]; + const viewport2 = [1050, 800]; + const single1 = [728, 90]; + const single2 = [970, 90]; + const multi = [single1, single2]; + + /** @test {SizeMappingArray#isSizeMappingArray} */ + describe('#isSizeMappingArray', () => { + it('returns false for null', () => { + expect(SizeMappingArray.isSizeMappingArray(null)).to.be(false); + }); + + it('returns false for undefined', () => { + expect(SizeMappingArray.isSizeMappingArray(undefined)).to.be(false); + }); + + it('returns false for "fluid"', () => { + expect(SizeMappingArray.isSizeMappingArray('fluid')).to.be(false); + }); + + it('returns true for valid mapping', () => { + expect(SizeMappingArray.isSizeMappingArray([ + [viewport1, multi], + [viewport2, single2] + ])).to.be(true); + }); + + it('returns false for invalid viewport', () => { + expect(SizeMappingArray.isSizeMappingArray([ + [viewport1, multi], + ['fluid', single2] + ])).to.be(false); + }); + }); +}); diff --git a/test/unit/SizeMappingBuilder.spec.js b/test/unit/SizeMappingBuilder.spec.js index 81b1469..4014b71 100644 --- a/test/unit/SizeMappingBuilder.spec.js +++ b/test/unit/SizeMappingBuilder.spec.js @@ -1,6 +1,8 @@ import SizeMappingBuilder from '../../src/SizeMappingBuilder'; +/** @test {SizeMappingBuilder} */ describe('SizeMappingBuilder', () => { + /** @test {SizeMappingBuilder#constructor} */ describe('#constructor', () => { it('constructs', () => { const builder = new SizeMappingBuilder(); @@ -11,6 +13,7 @@ describe('SizeMappingBuilder', () => { }); }); + /** @test {SizeMappingBuilder#addSize} */ describe('#addSize', () => { it('returns the builder', () => { const builder = new SizeMappingBuilder(); @@ -48,6 +51,7 @@ describe('SizeMappingBuilder', () => { }); }); + /** @test {SizeMappingBuilder#build} */ describe('#build', () => { it('returns null if no addSize call', () => { const builder = new SizeMappingBuilder(); diff --git a/test/unit/Slot.spec.js b/test/unit/Slot.spec.js index 6ce62a5..c8b79fe 100644 --- a/test/unit/Slot.spec.js +++ b/test/unit/Slot.spec.js @@ -3,14 +3,28 @@ import Slot from '../../src/Slot'; import Service from '../../src/Service'; import ResponseInformation from '../../src/ResponseInformation'; import SafeFrameConfig from '../../src/SafeFrameConfig'; +import SlotOnloadEvent from '../../src/events/SlotOnloadEvent'; +import SlotRenderEndedEvent from '../../src/events/SlotRenderEndedEvent'; +import ImpressionViewableEvent from '../../src/events/ImpressionViewableEvent'; +import SlotVisibilityChangedEvent from '../../src/events/SlotVisibilityChangedEvent'; +/** @test {Slot} */ describe('Slot', () => { - const adUnitPath = ''; - const optDiv = null; + const adUnitPath = '/Test/12345'; + const optDiv = 'gpt-div-123'; + const optDivNull = null; const size = [728, 90]; const gt = new GPT(); const clickUrl = 'http://www.google.com/'; - + const advertiserId = 'adv'; + const campaignId = 'camp'; + const lineItemId = 123; + const creativeId = 456; + const labelIds = ['label1', 'label2']; + const info = new ResponseInformation(advertiserId, campaignId, lineItemId, + creativeId, labelIds); + + /** @test {Slot#constructor} */ describe('#constructor', () => { it('constructs', () => { const slot = new Slot(adUnitPath, size, optDiv); @@ -18,6 +32,23 @@ describe('Slot', () => { }); }); + /** @test {Slot#getName} */ + describe('#getName', () => { + it('returns the adUnitPath', () => { + const slot = new Slot(adUnitPath, size, optDiv); + expect(slot.getName()).to.be(adUnitPath); + }); + }); + + /** @test {Slot#getDefinedId} */ + describe('#getDefinedId', () => { + it('returns the instance', () => { + const slot = new Slot(adUnitPath, size, optDiv, 123); + expect(slot.getDefinedId()).to.be(123); + }); + }); + + /** @test {Slot#getAdUnitPath} */ describe('#getAdUnitPath', () => { it('returns the adUnitPath', () => { const slot = new Slot(adUnitPath, size, optDiv); @@ -25,13 +56,20 @@ describe('Slot', () => { }); }); + /** @test {Slot#getSlotElementId} */ describe('#getSlotElementId', () => { it('returns the optDiv', () => { const slot = new Slot(adUnitPath, size, optDiv); expect(slot.getSlotElementId()).to.be(optDiv); }); + + it('returns a generated ID if necessary', () => { + const slot = new Slot(adUnitPath, size, optDivNull); + expect(slot.getSlotElementId()).to.be(`gpt_unit_${adUnitPath}_0`); + }); }); + /** @test {Slot#getServices} */ describe('#getServices', () => { it('returns the services', () => { const service = new Service(gt, 'test'); @@ -41,6 +79,7 @@ describe('Slot', () => { }); }); + /** @test {Slot#getSizes} */ describe('#getSizes', () => { it('returns the sizes', () => { const slot = new Slot(adUnitPath, size, optDiv); @@ -53,6 +92,7 @@ describe('Slot', () => { }); }); + /** @test {Slot#getOutOfPage} */ describe('#getOutOfPage', () => { it('returns the outOfPage status', () => { const slot = new Slot(adUnitPath, size, optDiv); @@ -63,6 +103,7 @@ describe('Slot', () => { }); }); + /** @test {Slot#display} */ describe('#display', () => { it('returns undefined', () => { const slot = new Slot(adUnitPath, size, optDiv); @@ -101,6 +142,7 @@ describe('Slot', () => { }); }); + /** @test {Slot#getAttributeKeys} */ describe('#getAttributeKeys', () => { it('returns the attribute keys', () => { const slot = new Slot(adUnitPath, size, optDiv); @@ -112,6 +154,18 @@ describe('Slot', () => { }); }); + /** @test {Slot#_getAttributes} */ + describe('#_getAttributes', () => { + it('returns the attribute map', () => { + const slot = new Slot(adUnitPath, size, optDiv); + slot.set('attr1', 'value1'); + expect(slot._getAttributes()).to.eql({ + attr1: 'value1' + }); + }); + }); + + /** @test {Slot#get} */ describe('#get', () => { it('returns null if attribute not set', () => { const slot = new Slot(adUnitPath, size, optDiv); @@ -125,6 +179,7 @@ describe('Slot', () => { }); }); + /** @test {Slot#set} */ describe('#set', () => { it('returns the slot', () => { const slot = new Slot(adUnitPath, size, optDiv); @@ -138,6 +193,7 @@ describe('Slot', () => { }); }); + /** @test {Slot#getTargeting} */ describe('#getTargeting', () => { it('returns the null is not set', () => { const slot = new Slot(adUnitPath, size, optDiv); @@ -153,6 +209,7 @@ describe('Slot', () => { }); }); + /** @test {Slot#getTargetingKeys} */ describe('#getTargetingKeys', () => { it('returns empty array if no keys', () => { const slot = new Slot(adUnitPath, size, optDiv); @@ -169,6 +226,21 @@ describe('Slot', () => { }); }); + /** @test {Slot#getTargetingMap} */ + describe('getTargetingMap', () => { + it('returns the targeting map', () => { + const slot = new Slot(adUnitPath, size, optDiv); + slot.setTargeting('kv1', 'value1'); + slot.setTargeting('kv2', 'value2'); + expect(slot.getTargetingMap()).to.be.an('object'); + expect(slot.getTargetingMap()).to.eql({ + kv1: ['value1'], + kv2: ['value2'] + }); + }); + }); + + /** @test {Slot#setTargeting} */ describe('#setTargeting', () => { it('returns the slot', () => { const slot = new Slot(adUnitPath, size, optDiv); @@ -182,6 +254,13 @@ describe('Slot', () => { expect(slot.getTargeting('kv1')).to.eql(['value']); }); + it('saves the array value', () => { + const slot = new Slot(adUnitPath, size, optDiv); + slot.setTargeting('kv1', ['value']); + expect(slot.getTargeting('kv1')).to.be.an('array'); + expect(slot.getTargeting('kv1')).to.eql(['value']); + }); + it('overwrites the previous value', () => { const slot = new Slot(adUnitPath, size, optDiv); slot.setTargeting('kv1', 'value1'); @@ -193,6 +272,7 @@ describe('Slot', () => { }); }); + /** @test {Slot#updateTargetingFromMap} */ describe('#updateTargetingFromMap', () => { it('returns the slot', () => { const slot = new Slot(adUnitPath, size, optDiv); @@ -225,6 +305,7 @@ describe('Slot', () => { }); }); + /** @test {Slot#clearTargeting} */ describe('#clearTargeting', () => { it('returns the slot', () => { const slot = new Slot(adUnitPath, size, optDiv); @@ -242,6 +323,7 @@ describe('Slot', () => { }); }); + /** @test {Slot#addService} */ describe('#addService', () => { it('returns the slot', () => { const slot = new Slot(adUnitPath, size, optDiv); @@ -267,6 +349,7 @@ describe('Slot', () => { })); }); + /** @test {Slot#getCategoryExclusions} */ describe('#getCategoryExclusions', () => { it('returns empty array if no exclusions', () => { const slot = new Slot(adUnitPath, size, optDiv); @@ -283,6 +366,7 @@ describe('Slot', () => { }); }); + /** @test {Slot#setCategoryExclusion} */ describe('#setCategoryExclusion', () => { it('returns the slot', () => { const slot = new Slot(adUnitPath, size, optDiv); @@ -300,6 +384,7 @@ describe('Slot', () => { }); }); + /** @test {Slot#clearCategoryExclusions} */ describe('#clearCategoryExclusions', () => { it('returns the slot', () => { const slot = new Slot(adUnitPath, size, optDiv); @@ -317,6 +402,7 @@ describe('Slot', () => { }); }); + /** @test {Slot#defineSizeMapping} */ describe('#defineSizeMapping', () => { it('returns the slot', () => { const slot = new Slot(adUnitPath, size, optDiv); @@ -329,8 +415,16 @@ describe('Slot', () => { slot.defineSizeMapping([]); expect(slot._sizeMapping).to.eql([]); }); + + it('throws an error on invalid size mapping', () => { + const slot = new Slot(adUnitPath, size, optDiv); + expect(slot._sizeMapping).to.be(null); + expect(() => slot.defineSizeMapping('bob')).to.throwError(); + expect(slot._sizeMapping).to.be(null); + }); }); + /** @test {Slot#setClickUrl} */ describe('#setClickUrl', () => { it('returns the slot', () => { const slot = new Slot(adUnitPath, size, optDiv); @@ -344,6 +438,7 @@ describe('Slot', () => { }); }); + /** @test {Slot#getClickUrl} */ describe('#getClickUrl', () => { it('returns null if no click URL', () => { const slot = new Slot(adUnitPath, size, optDiv); @@ -357,6 +452,7 @@ describe('Slot', () => { }); }); + /** @test {Slot#getResponseInformation} */ describe('#getResponseInformation', () => { it('returns null if no response information', () => { const slot = new Slot(adUnitPath, size, optDiv); @@ -365,18 +461,12 @@ describe('Slot', () => { it('returns previous response information if set', () => { const slot = new Slot(adUnitPath, size, optDiv); - const advertiserId = 'adv'; - const campaignId = 'camp'; - const lineItemId = 123; - const creativeId = 456; - const labelIds = ['label1', 'label2']; - const info = new ResponseInformation(advertiserId, campaignId, lineItemId, - creativeId, labelIds); slot._responseInformation = info; expect(slot.getResponseInformation()).to.be(info); }); }); + /** @test {Slot#setCollapseEmptyDiv} */ describe('#setCollapseEmptyDiv', () => { it('returns the slot', () => { const slot = new Slot(adUnitPath, size, optDiv); @@ -385,13 +475,13 @@ describe('Slot', () => { expect(slot.setCollapseEmptyDiv(true)).to.be(slot); }); - it('saves the option', () => { + it('saves the collapseEmptyDiv option', () => { const slot = new Slot(adUnitPath, size, optDiv); slot.setCollapseEmptyDiv(true); expect(slot._options.collapseEmptyDiv).to.be(true); }); - it('saves the beforeAdFetch option', () => { + it('saves the collapseBeforeAdFetch option', () => { const slot = new Slot(adUnitPath, size, optDiv); slot.setCollapseEmptyDiv(true, true); expect(slot._options.collapseEmptyDiv).to.be(true); @@ -399,6 +489,17 @@ describe('Slot', () => { }); }); + /** @test {Slot#getCollapseEmptyDiv} */ + describe('#getCollapseEmptyDiv', () => { + it('returns the collapseEmptyDiv options', () => { + const slot = new Slot(adUnitPath, size, optDiv); + expect(slot.getCollapseEmptyDiv()).to.be(false); + slot.setCollapseEmptyDiv(true); + expect(slot.getCollapseEmptyDiv()).to.be(true); + }); + }); + + /** @test {Slot#setForceSafeFrame} */ describe('#setForceSafeFrame', () => { it('returns the slot', () => { const slot = new Slot(adUnitPath, size, optDiv); @@ -412,6 +513,7 @@ describe('Slot', () => { }); }); + /** @test {Slot#setSafeFrameConfig} */ describe('#setSafeFrameConfig', () => { it('returns the slot', () => { const allowOverlayExpansion = true; @@ -433,4 +535,199 @@ describe('Slot', () => { }); }); + /** @test {Slot#fetchStarted} */ + describe('#fetchStarted', () => { + it('clears the fetched option', () => { + const slot = new Slot(adUnitPath, size, optDiv); + slot._options.fetched = true; + slot.fetchStarted(); + expect(slot._options.fetched).to.be(false); + }); + }); + + /** @test {Slot#fetchEnded} */ + describe('#fetchEnded', () => { + it('sets the fetched option', () => { + const slot = new Slot(adUnitPath, size, optDiv); + expect(slot._options.fetched).to.be(false); + slot.fetchStarted(); + slot.fetchEnded(); + expect(slot._options.fetched).to.be(true); + }); + }); + + /** @test {Slot#loaded} */ + describe('#loaded', () => { + it('broadcasts the event', () => { + const service = new Service(gt, 'test'); + const slot = new Slot(adUnitPath, size, optDiv).addService(service); + const spy = sinon.spy(); + service.addEventListener('googletag.events.SlotOnloadEvent', spy); + slot.loaded(); + expect(spy.called).to.be(true); + expect(spy.calledWith(new SlotOnloadEvent(service.getName(), slot))).to.be(true); + }); + }); + + /** @test {Slot#renderStarted} */ + describe('#renderStarted', () => { + it('clears the displayed option', () => { + const slot = new Slot(adUnitPath, size, optDiv); + slot._options.displayed = true; + slot.renderStarted(); + expect(slot._options.displayed).to.be(false); + }); + }); + + /** @test {Slot#renderEnded} */ + describe('#renderEnded', () => { + it('sets the displayed option', () => { + const slot = new Slot(adUnitPath, size, optDiv); + slot.renderStarted(); + slot.renderEnded(); + expect(slot._options.displayed).to.be(true); + }); + + it('sets the response information', () => { + const slot = new Slot(adUnitPath, size, optDiv); + slot.renderStarted(); + slot.renderEnded(info); + expect(slot._responseInformation).to.be(info); + }); + + it('broadcasts an empty event if null', () => { + const service = new Service(gt, 'test'); + const slot = new Slot(adUnitPath, size, optDiv).addService(service); + const spy = sinon.spy(); + service.addEventListener('googletag.events.SlotRenderEndedEvent', spy); + slot.renderStarted(); + slot.renderEnded(null); + expect(spy.called).to.be(true); + expect(spy.calledWith(new SlotRenderEndedEvent(service.getName(), slot, + null, null, true, slot.getSizes()[0]))).to.be(true); + }); + + it('broadcasts a non-empty event if info provided', () => { + const service = new Service(gt, 'test'); + const slot = new Slot(adUnitPath, size, optDiv).addService(service); + const spy = sinon.spy(); + service.addEventListener('googletag.events.SlotRenderEndedEvent', spy); + slot.renderStarted(); + slot.renderEnded(info); + expect(spy.called).to.be(true); + expect(spy.calledWith(new SlotRenderEndedEvent(service.getName(), slot, + info.creativeId, info.lineItemId, false, slot.getSizes()[0]))).to.be(true); + }); + }); + + /** @test {Slot#impressionViewable} */ + describe('#impressionViewable', () => { + it('broadcasts the event', () => { + const service = new Service(gt, 'test'); + const slot = new Slot(adUnitPath, size, optDiv).addService(service); + const spy = sinon.spy(); + service.addEventListener('googletag.events.ImpressionViewableEvent', spy); + slot.impressionViewable(); + expect(spy.called).to.be(true); + expect(spy.calledWith(new ImpressionViewableEvent(service.getName(), slot))).to.be(true); + }); + }); + + /** @test {Slot#visibilityChanged} */ + describe('#visibilityChanged', () => { + it('saves the in-view percentage and broadcasts event', () => { + const inViewPercentage = 23; + const service = new Service(gt, 'test'); + const slot = new Slot(adUnitPath, size, optDiv).addService(service); + const spy = sinon.spy(); + service.addEventListener('googletag.events.SlotVisibilityChangedEvent', spy); + slot.visibilityChanged(inViewPercentage); + expect(slot._options.inViewPercentage).to.be(inViewPercentage); + expect(spy.called).to.be(true); + expect(spy.calledWith(new SlotVisibilityChangedEvent(service.getName(), slot, inViewPercentage))).to.be(true); + }); + + it('does not re-broadcast event if no change', () => { + const inViewPercentage = 23; + const service = new Service(gt, 'test'); + const slot = new Slot(adUnitPath, size, optDiv).addService(service); + const spy = sinon.spy(); + slot.visibilityChanged(inViewPercentage); + service.addEventListener('googletag.events.SlotVisibilityChangedEvent', spy); + expect(slot._options.inViewPercentage).to.be(inViewPercentage); + slot.impressionViewable(inViewPercentage); + expect(slot._options.inViewPercentage).to.be(inViewPercentage); + expect(spy.called).to.be(false); + }); + }); + + /** @test {Slot#_refresh} */ + describe('#_refresh', () => { + it('fetches the slot', () => { + const slot = new Slot(adUnitPath, size, optDiv); + expect(slot._options.fetched).to.be(false); + slot._refresh(); + expect(slot._options.fetched).to.be(true); + }); + + it('increments a refresh counter', () => { + const slot = new Slot(adUnitPath, size, optDiv); + expect(slot._options.refreshed).to.be(0); + slot._refresh(); + expect(slot._options.refreshed).to.be(1); + }); + }); + + /** @test {Slot#_clear} */ + describe('#_clear', () => { + it('clears the content', () => { + const content = 'CONTENT'; + const slot = new Slot(adUnitPath, size, optDiv); + slot._setContent(content); + slot._clear(); + expect(slot._options.content).to.be(null); + }); + + it('marks the slot as not fetched', () => { + const content = 'CONTENT'; + const slot = new Slot(adUnitPath, size, optDiv); + slot._setContent(content); + slot._clear(); + expect(slot._options.fetched).to.be(false); + }); + }); + + /** @test {Slot#_setContent} */ + describe('#_setContent', () => { + it('saves the content', () => { + const content = 'CONTENT'; + const slot = new Slot(adUnitPath, size, optDiv); + slot._setContent(content); + expect(slot._options.content).to.be(content); + }); + }); + + /** @test {Slot#_getContent} */ + describe('#_getContent', () => { + it('returns the content', () => { + const content = 'CONTENT'; + const slot = new Slot(adUnitPath, size, optDiv); + slot._setContent(content); + expect(slot._getContent()).to.be(content); + }); + }); + + /** @test {Slot#_removeServices} */ + describe('#_removeServices', () => { + it('removes all the services', () => { + const service = new Service(gt, 'test'); + const slot = new Slot(adUnitPath, size, optDiv).addService(service); + expect(slot.getServices()).to.be.an('array'); + expect(slot.getServices()).to.be.eql([service]); + slot._removeServices(); + expect(slot.getServices()).to.be.an('array'); + expect(slot.getServices()).to.be.empty(); + }); + }); + }); diff --git a/test/unit/SlotId.spec.js b/test/unit/SlotId.spec.js new file mode 100644 index 0000000..3e45846 --- /dev/null +++ b/test/unit/SlotId.spec.js @@ -0,0 +1,80 @@ +import SlotId from '../../src/SlotId'; + +/** @test {SlotId} */ +describe('SlotId', () => { + const adUnitPath = '/Test/12345'; + const instance = 123; + const optDomId = 'gpt-div-123'; + + /** @test {SlotId#constructor} */ + describe('#constructor', () => { + it('constructs', () => { + const slotId = new SlotId(adUnitPath, instance, optDomId); + expect(slotId).to.be.ok(); + expect(slotId).to.have.property('_adUnitPath', adUnitPath); + expect(slotId).to.have.property('_instance', instance); + expect(slotId).to.have.property('_domId', optDomId); + }); + + it('generates a dom ID if required', () => { + const slotId = new SlotId(adUnitPath, instance); + expect(slotId).to.be.ok(); + expect(slotId).to.have.property('_adUnitPath', adUnitPath); + expect(slotId).to.have.property('_instance', instance); + expect(slotId).to.have.property('_domId', 'gpt_unit_/Test/12345_123'); + }); + }); + + /** @test {SlotId#getId} */ + describe('#getId', () => { + it('returns the identifier', () => { + const slotId = new SlotId(adUnitPath, instance, optDomId); + expect(slotId.getId()).to.be(`${adUnitPath}_${instance}`); + }); + }); + + /** @test {SlotId#getInstance} */ + describe('#getInstance', () => { + it('returns the instance', () => { + const slotId = new SlotId(adUnitPath, instance, optDomId); + expect(slotId.getInstance()).to.be(instance); + }); + }); + + /** @test {SlotId#getName} */ + describe('#getName', () => { + it('returns the AdUnitPath', () => { + const slotId = new SlotId(adUnitPath, instance, optDomId); + expect(slotId.getName()).to.be(adUnitPath); + }); + }); + + /** @test {SlotId#getAdUnitPath} */ + describe('#getAdUnitPath', () => { + it('returns the AdUnitPath', () => { + const slotId = new SlotId(adUnitPath, instance, optDomId); + expect(slotId.getAdUnitPath()).to.be(adUnitPath); + }); + }); + + /** @test {SlotId#getDomId} */ + describe('#getDomId', () => { + it('returns the DOM ID passed in', () => { + const slotId = new SlotId(adUnitPath, instance, optDomId); + expect(slotId.getDomId()).to.be(optDomId); + }); + + it('returns the generated DOM ID', () => { + const slotId = new SlotId(adUnitPath, instance); + expect(slotId.getDomId()).to.be('gpt_unit_/Test/12345_123'); + }); + }); + + /** @test {SlotId#toString} */ + describe('#toString', () => { + it('returns the identifier', () => { + const slotId = new SlotId(adUnitPath, instance, optDomId); + expect(slotId.toString()).to.be(`${adUnitPath}_${instance}`); + }); + }); +}); diff --git a/test/unit/TargetingMap.spec.js b/test/unit/TargetingMap.spec.js new file mode 100644 index 0000000..6c9cf3a --- /dev/null +++ b/test/unit/TargetingMap.spec.js @@ -0,0 +1,111 @@ +import TargetingMap from '../../src/TargetingMap'; + +/** @test {TargetingMap} */ +describe('TargetingMap', () => { + /** @test {TargetingMap#get} */ + describe('#get', () => { + it('returns empty array if not set', () => { + const map = new TargetingMap(); + expect(map.get('kv1')).to.be.an('array'); + expect(map.get('kv1')).to.be.empty(); + }); + + it('returns the value (as an array) if set', () => { + const map = new TargetingMap(); + map.set('kv1', 'value'); + expect(map.get('kv1')).to.be.an('array'); + expect(map.get('kv1')).to.eql(['value']); + }); + }); + + /** @test {TargetingMap#keys} */ + describe('#keys', () => { + it('returns empty array if no keys', () => { + const map = new TargetingMap(); + expect(map.keys()).to.be.an('array'); + expect(map.keys()).to.be.empty(); + }); + + it('returns array of keys', () => { + const map = new TargetingMap(); + map.set('kv1', 'value1'); + map.set('kv2', 'value2'); + expect(map.keys()).to.be.an('array'); + expect(map.keys()).to.eql(['kv1', 'kv2']); + }); + }); + + /** @test {TargetingMap#all} */ + describe('all', () => { + it('returns the targeting map', () => { + const map = new TargetingMap(); + map.set('kv1', 'value1'); + map.set('kv2', 'value2'); + expect(map.all()).to.be.an('object'); + expect(map.all()).to.eql({ + kv1: ['value1'], + kv2: ['value2'] + }); + }); + }); + + /** @test {TargetingMap#set} */ + describe('#set', () => { + it('saves the value', () => { + const map = new TargetingMap(); + map.set('kv1', 'value'); + expect(map.get('kv1')).to.be.an('array'); + expect(map.get('kv1')).to.eql(['value']); + }); + + it('saves the array value', () => { + const map = new TargetingMap(); + map.set('kv1', ['value']); + expect(map.get('kv1')).to.be.an('array'); + expect(map.get('kv1')).to.eql(['value']); + }); + + it('overwrites the previous value', () => { + const map = new TargetingMap(); + map.set('kv1', 'value1'); + expect(map.get('kv1')).to.be.an('array'); + expect(map.get('kv1')).to.eql(['value1']); + map.set('kv1', 'value2'); + expect(map.get('kv1')).to.be.an('array'); + expect(map.get('kv1')).to.eql(['value2']); + }); + }); + + /** @test {TargetingMap#clear} */ + describe('#clear', () => { + it('clears all targeting', () => { + const map = new TargetingMap(); + map.set('kv1', 'value1'); + map.set('kv2', 'value2'); + expect(map.get('kv1')).to.be.an('array'); + expect(map.get('kv1')).to.eql(['value1']); + expect(map.get('kv2')).to.be.an('array'); + expect(map.get('kv2')).to.eql(['value2']); + map.clear(); + expect(map.get('kv1')).to.be.an('array'); + expect(map.get('kv1')).to.empty(); + expect(map.get('kv2')).to.be.an('array'); + expect(map.get('kv2')).to.empty(); + }); + + it('clears specific targeting', () => { + const map = new TargetingMap(); + map.set('kv1', 'value1'); + map.set('kv2', 'value2'); + expect(map.get('kv1')).to.be.an('array'); + expect(map.get('kv1')).to.eql(['value1']); + expect(map.get('kv2')).to.be.an('array'); + expect(map.get('kv2')).to.eql(['value2']); + map.clear('kv2'); + expect(map.get('kv1')).to.be.an('array'); + expect(map.get('kv1')).to.eql(['value1']); + expect(map.get('kv2')).to.be.an('array'); + expect(map.get('kv2')).to.empty(); + }); + }); +}); diff --git a/test/unit/events/ImpressionViewableEvent.spec.js b/test/unit/events/ImpressionViewableEvent.spec.js new file mode 100644 index 0000000..fa89b6b --- /dev/null +++ b/test/unit/events/ImpressionViewableEvent.spec.js @@ -0,0 +1,39 @@ +import ImpressionViewableEvent from '../../../src/events/ImpressionViewableEvent'; + +/** @test {ImpressionViewableEvent} */ +describe('ImpressionViewableEvent', () => { + const serviceName = 'testService'; + const slot = 'slot1'; + + /** @test {ImpressionViewableEvent#constructor} */ + describe('#constructor', () => { + it('constructs', () => { + const event = new ImpressionViewableEvent(serviceName, slot); + expect(event).to.be.ok(); + }); + }); + + /** @test {ImpressionViewableEvent#_name} */ + describe('#_name', () => { + it('returns name', () => { + const event = new ImpressionViewableEvent(serviceName, slot); + expect(event._name).to.be('googletag.events.ImpressionViewableEvent'); + }); + }); + + /** @test {ImpressionViewableEvent#serviceName} */ + describe('#serviceName', () => { + it('returns serviceName', () => { + const event = new ImpressionViewableEvent(serviceName, slot); + expect(event.serviceName).to.be(serviceName); + }); + }); + + /** @test {ImpressionViewableEvent#slot} */ + describe('#slot', () => { + it('returns slot', () => { + const event = new ImpressionViewableEvent(serviceName, slot); + expect(event.slot).to.be(slot); + }); + }); +}); diff --git a/test/unit/events/SlotOnloadEvent.spec.js b/test/unit/events/SlotOnloadEvent.spec.js new file mode 100644 index 0000000..7bacdb9 --- /dev/null +++ b/test/unit/events/SlotOnloadEvent.spec.js @@ -0,0 +1,39 @@ +import SlotOnloadEvent from '../../../src/events/SlotOnloadEvent'; + +/** @test {SlotOnloadEvent} */ +describe('SlotOnloadEvent', () => { + const serviceName = 'testService'; + const slot = 'slot1'; + + /** @test {SlotOnloadEvent#constructor} */ + describe('#constructor', () => { + it('constructs', () => { + const event = new SlotOnloadEvent(serviceName, slot); + expect(event).to.be.ok(); + }); + }); + + /** @test {SlotOnloadEvent#_name} */ + describe('#_name', () => { + it('returns _name', () => { + const event = new SlotOnloadEvent(serviceName, slot); + expect(event._name).to.be('googletag.events.SlotOnloadEvent'); + }); + }); + + /** @test {SlotOnloadEvent#serviceName} */ + describe('#serviceName', () => { + it('returns serviceName', () => { + const event = new SlotOnloadEvent(serviceName, slot); + expect(event.serviceName).to.be(serviceName); + }); + }); + + /** @test {SlotOnloadEvent#slot} */ + describe('#slot', () => { + it('returns slot', () => { + const event = new SlotOnloadEvent(serviceName, slot); + expect(event.slot).to.be(slot); + }); + }); +}); diff --git a/test/unit/events/SlotRenderEndedEvent.spec.js b/test/unit/events/SlotRenderEndedEvent.spec.js new file mode 100644 index 0000000..229e4f9 --- /dev/null +++ b/test/unit/events/SlotRenderEndedEvent.spec.js @@ -0,0 +1,79 @@ +import SlotRenderEndedEvent from '../../../src/events/SlotRenderEndedEvent'; + +/** @test {SlotRenderEndedEvent} */ +describe('SlotRenderEndedEvent', () => { + const serviceName = 'testService'; + const slot = 'slot1'; + const creativeId = 12345; + const lineItemId = 45673; + const isEmpty = false; + const size = [728, 90]; + + function givenAnEvent() { + return new SlotRenderEndedEvent(serviceName, slot, creativeId, lineItemId, isEmpty, size); + } + + /** @test {SlotRenderEndedEvent#constructor} */ + describe('#constructor', () => { + it('constructs', () => { + const event = givenAnEvent(); + expect(event).to.be.ok(); + }); + }); + + /** @test {SlotRenderEndedEvent#_name} */ + describe('#_name', () => { + it('returns _name', () => { + const event = givenAnEvent(); + expect(event._name).to.be('googletag.events.SlotRenderEndedEvent'); + }); + }); + + /** @test {SlotRenderEndedEvent#serviceName} */ + describe('#serviceName', () => { + it('returns serviceName', () => { + const event = givenAnEvent(); + expect(event.serviceName).to.be(serviceName); + }); + }); + + /** @test {SlotRenderEndedEvent#slot} */ + describe('#slot', () => { + it('returns slot', () => { + const event = givenAnEvent(); + expect(event.slot).to.be(slot); + }); + }); + + /** @test {SlotRenderEndedEvent#creativeId} */ + describe('#creativeId', () => { + it('returns creativeId', () => { + const event = givenAnEvent(); + expect(event.creativeId).to.be(creativeId); + }); + }); + + /** @test {SlotRenderEndedEvent#isEmpty} */ + describe('#isEmpty', () => { + it('returns isEmpty', () => { + const event = givenAnEvent(); + expect(event.isEmpty).to.be(isEmpty); + }); + }); + + /** @test {SlotRenderEndedEvent#lineItemId} */ + describe('#lineItemId', () => { + it('returns lineItemId', () => { + const event = givenAnEvent(); + expect(event.lineItemId).to.be(lineItemId); + }); + }); + + /** @test {SlotRenderEndedEvent#size} */ + describe('#size', () => { + it('returns size', () => { + const event = givenAnEvent(); + expect(event.size).to.be(size); + }); + }); +}); diff --git a/test/unit/events/SlotVisibilityChangedEvent.spec.js b/test/unit/events/SlotVisibilityChangedEvent.spec.js new file mode 100644 index 0000000..91a6175 --- /dev/null +++ b/test/unit/events/SlotVisibilityChangedEvent.spec.js @@ -0,0 +1,48 @@ +import SlotVisibilityChangedEvent from '../../../src/events/SlotVisibilityChangedEvent'; + +/** @test {SlotVisibilityChangedEvent} */ +describe('SlotVisibilityChangedEvent', () => { + const serviceName = 'testService'; + const slot = 'slot1'; + const inViewPercentage = 85; + + /** @test {SlotVisibilityChangedEvent#constructor} */ + describe('#constructor', () => { + it('constructs', () => { + const event = new SlotVisibilityChangedEvent(serviceName, slot, inViewPercentage); + expect(event).to.be.ok(); + }); + }); + + /** @test {SlotVisibilityChangedEvent#_name} */ + describe('#_name', () => { + it('returns _name', () => { + const event = new SlotVisibilityChangedEvent(serviceName, slot, inViewPercentage); + expect(event._name).to.be('googletag.events.SlotVisibilityChangedEvent'); + }); + }); + + /** @test {SlotVisibilityChangedEvent#serviceName} */ + describe('#serviceName', () => { + it('returns serviceName', () => { + const event = new SlotVisibilityChangedEvent(serviceName, slot, inViewPercentage); + expect(event.serviceName).to.be(serviceName); + }); + }); + + /** @test {SlotVisibilityChangedEvent#slot} */ + describe('#slot', () => { + it('returns slot', () => { + const event = new SlotVisibilityChangedEvent(serviceName, slot, inViewPercentage); + expect(event.slot).to.be(slot); + }); + }); + + /** @test {SlotVisibilityChangedEvent#inViewPercentage} */ + describe('#inViewPercentage', () => { + it('returns inViewPercentage', () => { + const event = new SlotVisibilityChangedEvent(serviceName, slot, inViewPercentage); + expect(event.inViewPercentage).to.be(inViewPercentage); + }); + }); +});