Home Reference Source Test Repository

unit/model/recommendation.js

import fetchMock from 'fetch-mock';
import mockery from 'mockery';
import chai from 'chai';
import NprOne from '../../../src/index';
import Recommendation from '../../../src/model/recommendation';
import { LISTENING_V2_RECOMMENDATIONS_RESPONSE } from '../../test-data';


/** @test {Recommendation} */
describe('Recommendation', () => {
    const response = LISTENING_V2_RECOMMENDATIONS_RESPONSE;
    const adsWizzUrl = '^https://demo.adswizz.com';
    const doubleClickUrl = '^https://ad.doubleclick.net';
    /** @type {Recommendation} */
    let recStationId;
    /** @type {Recommendation} */
    let recNewscast;
    /** @type {Recommendation} */
    let recStoryA;
    /** @type {Recommendation} */
    let recAd;
    /** @type {Recommendation} */
    let recPromo;
    /** @type {Recommendation} */
    let recStoryB;

    const setupMockery = () => {
        mockery.registerMock('fetch', fetchMock
            .mock(adsWizzUrl, 'GET')
            .mock(doubleClickUrl, 'GET')
            .getMock());
    };

    beforeEach(() => {
        setupMockery();

        recStationId = new Recommendation(response.items[0]);
        recNewscast = new Recommendation(response.items[1]);
        recStoryA = new Recommendation(response.items[2]);
        recAd = new Recommendation(response.items[3]);
        recPromo = new Recommendation(response.items[4]);
        recStoryB = new Recommendation(response.items[5]);
    });

    afterEach(() => {
        fetchMock.restore();
        mockery.deregisterMock('fetch');
    });

    /** @test {Recommendation#constructor} */
    describe('constructor', () => {
        let responseClone;

        beforeEach(() => {
            responseClone = JSON.parse(JSON.stringify(response.items[0]));
        });


        it('should throw an error if audio doesn\'t exist', () => {
            delete responseClone.links.audio;
            chai.expect(() => {
                new Recommendation(responseClone);
            }).to.throw('Audio must exist within links.');
        });

        it('should throw an error if recommendations doesn\'t exist', () => {
            delete responseClone.links.recommendations;
            chai.expect(() => {
                new Recommendation(responseClone);
            }).to.throw('Recommendation (contains URL) must exist within links.');
        });

        it('should throw an error if attributes.rating doesn\'t exist', () => {
            delete responseClone.attributes.rating;
            chai.expect(() => {
                new Recommendation(responseClone);
            }).to.throw('Attributes must contain a rating object.');
        });
    });

    /** @test {Recommendation#getImages} */
    describe('getImages', () => {
        it('should match the API response', () => {
            recAd.getImages().should.equal(response.items[3].links.image);
        });
    });

    /** @test {Recommendation#getAudio} */
    describe('getAudio', () => {
        it('should match the API response', () => {
            recStoryA.getAudio().should.equal(response.items[2].links.audio);
        });
    });

    /** @test {Recommendation#getWeb} */
    describe('getWeb', () => {
        it('should match the API response', () => {
            recStoryA.getWeb().should.equal(response.items[2].links.web);
        });
    });

    /** @test {Recommendation#getOnRamps} */
    describe('getOnRamps', () => {
        it('should match the API response', () => {
            recStoryA.getOnRamps().should.equal(response.items[2].links.onramps);
        });
    });

    /** @test {Recommendation#getImpressions} */
    describe('getImpressions', () => {
        it('should match the API response', () => {
            recAd.getImpressions().should.equal(response.items[3].links.impression);
        });
    });

    /** @test {Recommendation#getCallsToAction} */
    describe('getCallsToAction', () => {
        it('should match the API response', () => {
            recPromo.getCallsToAction().should.equal(response.items[4].links.action);
        });
    });

    /** @test {Recommendation#getRelateds} */
    describe('getRelateds', () => {
        it('should match the API response', () => {
            recAd.getRelateds().should.equal(response.items[3].links.related);
        });
    });

    /** @test {Recommendation#getRelatedImpressions} */
    describe('getRelatedImpressions', () => {
        it('should match the API response', () => {
            recAd.getRelatedImpressions().should.equal(response.items[3].links['related-impression']);
        });
    });

    /** @test {Recommendation#getRatings} */
    describe('getRatings', () => {
        it('should return a ratings object', () => {
            recStationId.getRatings().should.be.ok;
        });
    });

    /** @test {Recommendation#getRecommendationUrl} */
    describe('getRecommendationUrl', () => {
        it('should match the API response', () => {
            recStationId.getRecommendationUrl().should.equal(response.items[0].links.recommendations[0].href);
        });
    });

    /** @test {Recommendation#getActionRecommendationUrl} */
    describe('getActionRecommendationUrl', () => {
        const actionRecUrl = 'https://listening.api.npr.org/v2/ratings?sharedMediaId=458726244%3A458726246&flow=5&channel=npr&prevStories=1&recommend=true';

        it('should match the API response', () => {
            recPromo.getActionRecommendationUrl().should.not.equal(response.items[4].links.action[0].href);
            recPromo.getActionRecommendationUrl().should.equal(actionRecUrl);
        });

        it('should only use urls that start with nprone://', () => {
            const promoClone = JSON.parse(JSON.stringify(response.items[4]));
            promoClone.links.action.unshift({ 'href': 'nprtwo://should-not?be-used' });
            const promo = new Recommendation(promoClone);
            promo.getActionRecommendationUrl().should.equal(actionRecUrl);
        });
    });

    /** @test {Recommendation#isSponsorship} */
    describe('isSponsorship', () => {
        it('should be true for an ad', () => {
            recAd.isSponsorship().should.be.true;
        });
        it('should be false for non ads', () => {
            recStationId.isSponsorship().should.be.false;
            recNewscast.isSponsorship().should.be.false;
            recStoryA.isSponsorship().should.be.false;
            recPromo.isSponsorship().should.be.false;
            recStoryB.isSponsorship().should.be.false;
        });
    });

    /** @test {Recommendation#isShareable} */
    describe('isShareable', () => {
        it('should be true for recommendations with onramps', () => {
            recStoryA.isShareable().should.be.true;
            recPromo.isShareable().should.be.true;
            recStoryB.isShareable().should.be.true;
        });
        it('should be false for recommendations without onramps', () => {
            recStationId.isShareable().should.be.false;
            recNewscast.isShareable().should.be.false;
            recAd.isShareable().should.be.false;
        });
    });

    /** @test {Recommendation#toString} */
    describe('toString', () => {
        it('should match the format specified', () => {
            recStoryA.toString().should.equal('[UID=466898631:466898632, R=]');
        });
    });

    /** @test {Recommendation#recordAction} */
    describe('recordAction', () => {
        it('should only allow valid ratings', () => {
            recNewscast.setRatingReceivedCallback(() => {});

            recNewscast.recordAction(NprOne.Action.START, 0);
            recNewscast.getRatings()[0].rating.should.equal('START');
            recNewscast.recordAction(NprOne.Action.COMPLETED, 10);
            recNewscast.getRatings()[1].rating.should.equal('COMPLETED');

            chai.expect(() => {
                recNewscast.recordAction('BAD_VALUE', 0);
            }).to.throw('BAD_VALUE action is invalid. See Action class for valid actions.');
        });

        it('should require elapsed time to be an integer', () => {
            recStationId.setRatingReceivedCallback(() => {});

            chai.expect(() => {
                recStationId.recordAction(NprOne.Action.START);
            }).to.throw('Elapsed time must be supplied and be a positive integer value.');

            chai.expect(() => {
                recStationId.recordAction(NprOne.Action.SKIP, 'bad value');
            }).to.throw('Elapsed time must be supplied and be a positive integer value.');
        });

        it('should auto-correct bad elapsed time values', () => {
            recStationId.setRatingReceivedCallback(() => {});

            recStationId.recordAction(NprOne.Action.START, -10);
            recStationId.getRatings()[0].elapsed.should.equal(0);
            recStationId.recordAction(NprOne.Action.START, 99999);
            recStationId.getRatings()[1].elapsed.should.equal(recStationId.attributes.duration);
        });

        it('should auto-correct bad elapsed time values but only if duration is greater than 0', () => {
            const clone = JSON.parse(JSON.stringify(response.items[0]));
            clone.attributes.duration = 0;

            const rec = new Recommendation(clone);

            rec.setRatingReceivedCallback(() => {});

            const largeDuration = 10000;

            rec.recordAction(NprOne.Action.COMPLETED, largeDuration);
            rec.getRatings()[0].elapsed.should.equal(largeDuration);
        });

        it('should warn developers when a zero elapsed time is given for a COMPLETED or SKIP', () => {
            recStationId.recordAction(NprOne.Action.COMPLETED, 0);
        });

        it('should warn developers when a non-START rating is received before START', () => {
            const clone = JSON.parse(JSON.stringify(response.items[0]));
            clone.attributes.duration = 0;

            const rec = new Recommendation(clone);

            rec.setRatingReceivedCallback(() => {});
            rec.recordAction(NprOne.Action.COMPLETED, 30);
        });

        it('should auto-correct bad elapsed time values if the difference is more than 30', () => {
            recStoryA.setRatingReceivedCallback(() => {});

            const moderatelyWrongDuration = recStoryA.attributes.duration + 20;

            recStoryA.recordAction(NprOne.Action.COMPLETED, moderatelyWrongDuration);
            recStoryA.getRatings()[0].elapsed.should.equal(recStoryA.attributes.duration);
        });

        it('should execute a GET request against all impression URLs', () => {
            recAd.setRatingReceivedCallback(() => {});

            recAd.recordAction(NprOne.Action.START, 0);

            fetchMock.called(adsWizzUrl).should.be.true;
            fetchMock.called(doubleClickUrl).should.be.true;
            fetchMock.calls().unmatched.length.should.equal(0);
        });

        it('but only on the first START rating and not subsequent STARTs', () => {
            recAd.setRatingReceivedCallback(() => {});
            recAd.recordAction(NprOne.Action.START, 0);

            fetchMock.restore();
            mockery.deregisterMock('fetch');
            setupMockery();

            recAd.recordAction(NprOne.Action.START, 0);

            fetchMock.called(adsWizzUrl).should.be.false;
            fetchMock.called(doubleClickUrl).should.be.false;
        });

        it('should call the callback set via setRatingReceivedCallback', done => {
            recStationId.setRatingReceivedCallback(() => {
                done();
            });
            recStationId.recordAction(NprOne.Action.START, 0);
        });
    });

    /** @test {Recommendation#hasAction} */
    describe('hasAction', () => {
        it('should be true when a recommendation actually has that rating', () => {
            recStoryA.setRatingReceivedCallback(() => {});
            recStoryA.recordAction(NprOne.Action.START, 0);
            recStoryA.hasAction(NprOne.Action.START).should.be.true;
        });

        it('should be false when a recommendation does not have that rating', () => {
            recStoryA.setRatingReceivedCallback(() => {});
            recStoryA.recordAction(NprOne.Action.COMPLETED, 0);

            recStoryA.hasAction(NprOne.Action.START).should.be.false;
        });
    });

    /** @test {Recommendation#hasEndAction} */
    describe('hasEndAction', () => {
        it('should be true when a recommendation has a finished rating', () => {
            recStoryA.setRatingReceivedCallback(() => {});
            recStoryA.recordAction(NprOne.Action.START, 0);
            recStoryA.recordAction(NprOne.Action.SHARE, 0);

            recStoryA.hasEndAction().should.be.false;

            recStoryA.recordAction(NprOne.Action.COMPLETED, 0);

            recStoryA.hasEndAction().should.be.true;
        });
    });
});