Skip to content

Commit

Permalink
[Component] GPT component (#26)
Browse files Browse the repository at this point in the history
* feature: add gpt component with support for header biding

* fix: deal with some issues in the gpt story

* fix: render ad inside storybook

Storybook relies on iframes to render stories, gpt map sizes, used to
build responsive ads don't seem to work with it. This hack solves it for
now until further developments on storybookjs/storybook#862

* chore: document gpt story hack

* chore: add tests for gpt, pbjs and ad managers

* chore: add more tests for ad manager

* add more tests to ad-manager

* fixes here and there (console.log, typos, etc)

* classes instead of functions

* corrected most comments [WIP]

* snapshot test

* change story to render two ads in article page

* quick refactor on ad manager test

* alternative using react broadcast

* fix tests [WIP]

* snapshot tests; pbjs config; tests adapted

* fix: add missing react-broadcast dep

* chore: redo initialisation checks on ad, gpt and pbjs managers

* remove old test

* fix: jest config on gpt component

* chore: remove unneeded JSDOM dev dep

* fix: add section as a prop of ad composer

* fix: return on all callbacks

* chore: lint gpt component

* fix: network id should be a prop of AdManager

* chore: add comments on gpt config

* chore: use promises on the gpt, pbjs and ad managers

* chore: remove errors on improper class usage

* CC linting err fixed; adUnit as prop

* remove new.target as class constructors need to be called with new anyway

* chore: increase coverage

* one more test

* chore: update jest configuration

* prebid settings unit tests

* fix: change test assumption titles

* fix: test message to remember to turn ad blocker off

* fix: throw error if slot does not exist

* fix: use storybook url and remove transform
  • Loading branch information
JGAntunes authored and Filipe Pinheiro committed Jul 14, 2017
1 parent 9ea3246 commit 43fbd16
Show file tree
Hide file tree
Showing 23 changed files with 4,607 additions and 40 deletions.
6 changes: 3 additions & 3 deletions .storybook/webpack.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@ module.exports = {
resolve: {
// Maps the 'react-native' import to 'react-native-web'.
alias: {
'react-native': 'react-native-web',
'@storybook/react-native': '@storybook/react',
"react-native": "react-native-web",
"@storybook/react-native": "@storybook/react"
},
// If you're working on a multi-platform React Native app, web-specific
// module implementations should be written in files using the extension
// `.web.js`.
extensions: ['.web.js', '.js', '.ios.js', '.android.js']
extensions: [".web.js", ".js", ".ios.js", ".android.js"]
}
};
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@
"url": "0.11.0"
},
"dependencies": {
"babel-plugin-transform-class": "^0.3.0",
"dashify": "0.2.2",
"global": "4.3.2",
"handlebars": "4.0.10",
Expand Down
29 changes: 29 additions & 0 deletions packages/gpt/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
BSD 3-Clause License

Copyright (c) 2017, News UK & Ireland Ltd
All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:

* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.

* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.

* Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
6 changes: 6 additions & 0 deletions packages/gpt/__tests__/.eslintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"env": {
"jest": true,
"browser": true
}
}
22 changes: 22 additions & 0 deletions packages/gpt/__tests__/__snapshots__/ad-composer.test.js.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`AdComposer renders no ad slots 1`] = `<div />`;

exports[`AdComposer renders with more than one ad slot 1`] = `
<div>
<div
id="ad-header"
/>
<div
id="intervention"
/>
</div>
`;

exports[`AdComposer renders with one ad slot 1`] = `
<div>
<div
id="ad-header"
/>
</div>
`;
13 changes: 13 additions & 0 deletions packages/gpt/__tests__/__snapshots__/gpt.test.js.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`Gpt test renders an ad-header ad slot 1`] = `
<div
id="ad-header"
/>
`;

exports[`Gpt test renders an ad-pixel ad slot 1`] = `
<div
id="ad-pixel"
/>
`;
38 changes: 38 additions & 0 deletions packages/gpt/__tests__/ad-composer.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import React from "react";
import renderer from "react-test-renderer";

import AdComposer from "../ad-composer";
import Ad from "../ad";

describe("AdComposer", () => {
it("renders no ad slots", () => {
const tree = renderer.create(<AdComposer section="article" />).toJSON();

expect(tree).toMatchSnapshot();
});

it("renders with one ad slot", () => {
const tree = renderer
.create(
<AdComposer section="article">
<Ad code="ad-header" />
</AdComposer>
)
.toJSON();

expect(tree).toMatchSnapshot();
});

it("renders with more than one ad slot", () => {
const tree = renderer
.create(
<AdComposer section="article">
<Ad code="ad-header" />
<Ad code="intervention" />
</AdComposer>
)
.toJSON();

expect(tree).toMatchSnapshot();
});
});
252 changes: 252 additions & 0 deletions packages/gpt/__tests__/ad-manager.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,252 @@
import AdManager from "../ad-manager";
import { getSlotConfig } from "../generate-config";
import gptManager from "../gpt-manager";
import pbjs from "../pbjs-manager";
import { pbjs as pbjsConfig } from "../config";

const pbjsManager = pbjs(pbjsConfig);

describe("AdManager", () => {
const managerOptions = {
adUnit: "mock-ad-unit",
networkId: "mock-network-id",
section: "mock-section",
gptManager,
pbjsManager,
getSlotConfig
};
let adManager;

beforeEach(() => {
adManager = new AdManager(managerOptions);
});

it("constructor returns an AdManager instance with correct props", () => {
expect(adManager).toBeInstanceOf(AdManager);
expect(adManager.initialised).toBeFalsy();
expect(adManager.adQueue).toHaveLength(0);
});

it("constructor returns an AdManager instance with adUnit", () => {
expect(adManager.adUnit).toBe(managerOptions.adUnit);
});

it("constructor returns an AdManager instance with networkId", () => {
expect(adManager.networkId).toBe(managerOptions.networkId);
});

it("constructor returns an AdManager instance with section", () => {
expect(adManager.section).toBe(managerOptions.section);
});

it("init function sets the required scripts", () => {
const newPbjsManager = adManager.pbjsManager;
const newGptManager = adManager.gptManager;

const pbjsLoadScript = jest.fn();
const gptLoadScript = jest.fn();

newPbjsManager.loadScript = pbjsLoadScript;
newGptManager.loadScript = gptLoadScript;

newPbjsManager.setConfig = () => Promise.resolve();
newGptManager.setConfig = () => Promise.resolve();
newPbjsManager.init = () => Promise.resolve();
newGptManager.init = () => Promise.resolve();
return adManager.init().then(() => {
expect(pbjsLoadScript).toHaveBeenCalled();
expect(gptLoadScript).toHaveBeenCalled();
expect(adManager.initialised).toBeTruthy();
});
});

it("registerAd inserts configured ad in the queue and push it to gpt on it", () => {
const newPbjsManager = adManager.pbjsManager;
const newGptManager = adManager.gptManager;

const windowWidth = 100;
const mockAd = {
code: "mock-code",
mappings: [100, 200],
options: { foo: "bar" }
};

adManager.getSlotConfig = jest
.fn()
.mockImplementation((section, code, width) => {
expect(section).toEqual(adManager.section);
expect(code).toEqual(mockAd.code);
expect(width).toEqual(windowWidth);
return mockAd;
});
adManager.pushAdToGPT = jest.fn();

adManager.registerAd(mockAd.code, { width: windowWidth });
expect(adManager.getSlotConfig).toHaveBeenCalled();
expect(adManager.adQueue).toHaveLength(1);
expect(adManager.adQueue[0]).toEqual(mockAd);

newPbjsManager.setConfig = () => Promise.resolve();
newGptManager.setConfig = () => Promise.resolve();
newPbjsManager.init = () => Promise.resolve();
newGptManager.init = () => Promise.resolve();
return adManager.init().then(() => {
expect(adManager.pushAdToGPT).toHaveBeenCalled();
});
});

it("unregister one ad", () => {
adManager.adQueue = [
{
id: "id-0"
},
{
id: "id-1"
}
];
const itemId = "id-1";
expect(adManager.adQueue.length).toEqual(2);
adManager.unregisterAd(itemId);
expect(adManager.adQueue.length).toEqual(1);
});

it("remove one item from the queue", () => {
const queue = [
{
id: "id-0"
},
{
id: "id-1"
}
];
const itemId = "id-1";
const newQueue = AdManager.removeItemFromQueue(queue, itemId);
expect(queue.length).toEqual(2);
expect(newQueue.length).toEqual(queue.length - 1);
});

it("display should tell pbjs to handle targeting and gpt to refresh", () => {
const newPbjsManager = adManager.pbjsManager;
const newGptManager = adManager.gptManager;
adManager.initialised = true;

const refresh = jest.fn();
const pubads = jest.fn().mockImplementation(() => ({
refresh
}));
newGptManager.googletag = {
cmd: [],
pubads
};

const setTargetingForGPTAsync = jest.fn();
newPbjsManager.pbjs = {
que: [],
setTargetingForGPTAsync
};

expect(() => {
adManager.display();
}).not.toThrowError();

newGptManager.googletag.cmd[0]();
newPbjsManager.pbjs.que[0]();
expect(pubads).toHaveBeenCalled();
expect(refresh).toHaveBeenCalled();
expect(setTargetingForGPTAsync).toHaveBeenCalled();
});

it("pushAdToGPT gives an error if ad manager is not initialised", () => {
expect(adManager.initialised).toEqual(false);
expect(adManager.pushAdToGPT).toThrowError();
});

it("pushAdToGPT creates and sets slot and asks gpt to display", () => {
const newGptManager = adManager.gptManager;

const addService = jest.fn();
const defineSizeMapping = jest.fn();
adManager.createSlot = jest.fn().mockImplementation(() => ({
addService,
defineSizeMapping
}));

const display = jest.fn();
const pubads = jest.fn();
newGptManager.googletag = {
cmd: [],
display,
pubads
};

const slotId = "mock-slot-id";
const sizingMap = [
{
width: 300,
height: 100,
sizes: [[320, 50], [320, 48]]
}
];

adManager.initialised = true;
adManager.generateSizings = jest.fn();
adManager.pushAdToGPT(slotId, sizingMap);
newGptManager.googletag.cmd[0]();
expect(addService).toHaveBeenCalled();
expect(defineSizeMapping).toHaveBeenCalled();
expect(display).toHaveBeenCalled();
expect(display).toHaveBeenCalledWith(slotId);
});

it("pushAdToGPT gives an error if slot does not exist", () => {
adManager.createSlot = jest.fn().mockImplementation(() => null);
const display = jest.fn();
adManager.gptManager.googletag = {
cmd: [],
display
};
adManager.pushAdToGPT();
adManager.gptManager.googletag.cmd[0]();
expect(display).not.toHaveBeenCalled();
});

it("generateSizings calls gpt googletag to set sizings", () => {
const newGptManager = adManager.gptManager;
adManager.initialised = true;

const addSize = jest.fn();
const build = jest.fn();
newGptManager.googletag = {
sizeMapping: jest.fn().mockImplementation(() => ({
addSize,
build
}))
};

const sizingMap = [
{
width: 300,
height: 100,
sizes: [[320, 50], [320, 48]]
}
];

adManager.generateSizings(sizingMap);
expect(newGptManager.googletag.sizeMapping).toHaveBeenCalled();
expect(addSize).toHaveBeenCalled();
expect(build).toHaveBeenCalled();
});

it("createSlot calls gpt googletag to set slots", () => {
const newGptManager = adManager.gptManager;
adManager.initialised = true;

newGptManager.googletag = {
defineSlot: jest.fn()
};

const slotId = "mock-slot-id";
adManager.createSlot(slotId, managerOptions.section);
expect(newGptManager.googletag.defineSlot).toHaveBeenCalled();
});
});
Loading

0 comments on commit 43fbd16

Please sign in to comment.