summaryrefslogtreecommitdiffstats
path: root/browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/ImpressionStats.test.jsx
diff options
context:
space:
mode:
Diffstat (limited to 'browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/ImpressionStats.test.jsx')
-rw-r--r--browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/ImpressionStats.test.jsx278
1 files changed, 278 insertions, 0 deletions
diff --git a/browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/ImpressionStats.test.jsx b/browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/ImpressionStats.test.jsx
new file mode 100644
index 0000000000..1d4778e342
--- /dev/null
+++ b/browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/ImpressionStats.test.jsx
@@ -0,0 +1,278 @@
+"use strict";
+
+import {
+ ImpressionStats,
+ INTERSECTION_RATIO,
+} from "content-src/components/DiscoveryStreamImpressionStats/ImpressionStats";
+import { actionTypes as at } from "common/Actions.sys.mjs";
+import React from "react";
+import { shallow } from "enzyme";
+
+describe("<ImpressionStats>", () => {
+ const SOURCE = "TEST_SOURCE";
+ const FullIntersectEntries = [
+ { isIntersecting: true, intersectionRatio: INTERSECTION_RATIO },
+ ];
+ const ZeroIntersectEntries = [
+ { isIntersecting: false, intersectionRatio: 0 },
+ ];
+ const PartialIntersectEntries = [
+ { isIntersecting: true, intersectionRatio: INTERSECTION_RATIO / 2 },
+ ];
+
+ // Build IntersectionObserver class with the arg `entries` for the intersect callback.
+ function buildIntersectionObserver(entries) {
+ return class {
+ constructor(callback) {
+ this.callback = callback;
+ }
+
+ observe() {
+ this.callback(entries);
+ }
+
+ unobserve() {}
+ };
+ }
+
+ const DEFAULT_PROPS = {
+ rows: [
+ { id: 1, pos: 0 },
+ { id: 2, pos: 1 },
+ { id: 3, pos: 2 },
+ ],
+ source: SOURCE,
+ IntersectionObserver: buildIntersectionObserver(FullIntersectEntries),
+ document: {
+ visibilityState: "visible",
+ addEventListener: sinon.stub(),
+ removeEventListener: sinon.stub(),
+ },
+ };
+
+ const InnerEl = () => <div>Inner Element</div>;
+
+ function renderImpressionStats(props = {}) {
+ return shallow(
+ <ImpressionStats {...DEFAULT_PROPS} {...props}>
+ <InnerEl />
+ </ImpressionStats>
+ );
+ }
+
+ it("should render props.children", () => {
+ const wrapper = renderImpressionStats();
+ assert.ok(wrapper.contains(<InnerEl />));
+ });
+ it("should not send loaded content nor impression when the page is not visible", () => {
+ const dispatch = sinon.spy();
+ const props = {
+ dispatch,
+ document: {
+ visibilityState: "hidden",
+ addEventListener: sinon.spy(),
+ removeEventListener: sinon.spy(),
+ },
+ };
+ renderImpressionStats(props);
+
+ assert.notCalled(dispatch);
+ });
+ it("should noly send loaded content but not impression when the wrapped item is not visbible", () => {
+ const dispatch = sinon.spy();
+ const props = {
+ dispatch,
+ IntersectionObserver: buildIntersectionObserver(ZeroIntersectEntries),
+ };
+ renderImpressionStats(props);
+
+ // This one is for loaded content.
+ assert.calledOnce(dispatch);
+ const [action] = dispatch.firstCall.args;
+ assert.equal(action.type, at.DISCOVERY_STREAM_LOADED_CONTENT);
+ assert.equal(action.data.source, SOURCE);
+ assert.deepEqual(action.data.tiles, [
+ { id: 1, pos: 0 },
+ { id: 2, pos: 1 },
+ { id: 3, pos: 2 },
+ ]);
+ });
+ it("should not send impression when the wrapped item is visbible but below the ratio", () => {
+ const dispatch = sinon.spy();
+ const props = {
+ dispatch,
+ IntersectionObserver: buildIntersectionObserver(PartialIntersectEntries),
+ };
+ renderImpressionStats(props);
+
+ // This one is for loaded content.
+ assert.calledOnce(dispatch);
+ });
+ it("should send a loaded content and an impression when the page is visible and the wrapped item meets the visibility ratio", () => {
+ const dispatch = sinon.spy();
+ const props = {
+ dispatch,
+ IntersectionObserver: buildIntersectionObserver(FullIntersectEntries),
+ };
+ renderImpressionStats(props);
+
+ assert.calledTwice(dispatch);
+
+ let [action] = dispatch.firstCall.args;
+ assert.equal(action.type, at.DISCOVERY_STREAM_LOADED_CONTENT);
+ assert.equal(action.data.source, SOURCE);
+ assert.deepEqual(action.data.tiles, [
+ { id: 1, pos: 0 },
+ { id: 2, pos: 1 },
+ { id: 3, pos: 2 },
+ ]);
+
+ [action] = dispatch.secondCall.args;
+ assert.equal(action.type, at.DISCOVERY_STREAM_IMPRESSION_STATS);
+ assert.equal(action.data.source, SOURCE);
+ assert.deepEqual(action.data.tiles, [
+ { id: 1, pos: 0, type: "organic" },
+ { id: 2, pos: 1, type: "organic" },
+ { id: 3, pos: 2, type: "organic" },
+ ]);
+ });
+ it("should send a DISCOVERY_STREAM_SPOC_IMPRESSION when the wrapped item has a flightId", () => {
+ const dispatch = sinon.spy();
+ const flightId = "a_flight_id";
+ const props = {
+ dispatch,
+ flightId,
+ rows: [{ id: 1, pos: 1, advertiser: "test advertiser" }],
+ source: "TOP_SITES",
+ IntersectionObserver: buildIntersectionObserver(FullIntersectEntries),
+ };
+ renderImpressionStats(props);
+
+ // Loaded content + DISCOVERY_STREAM_SPOC_IMPRESSION + TOP_SITES_SPONSORED_IMPRESSION_STATS + impression
+ assert.callCount(dispatch, 4);
+
+ const [action] = dispatch.secondCall.args;
+ assert.equal(action.type, at.DISCOVERY_STREAM_SPOC_IMPRESSION);
+ assert.deepEqual(action.data, { flightId });
+ });
+ it("should send a TOP_SITES_SPONSORED_IMPRESSION_STATS when the wrapped item has a flightId", () => {
+ const dispatch = sinon.spy();
+ const flightId = "a_flight_id";
+ const props = {
+ dispatch,
+ flightId,
+ rows: [{ id: 1, pos: 1, advertiser: "test advertiser" }],
+ source: "TOP_SITES",
+ IntersectionObserver: buildIntersectionObserver(FullIntersectEntries),
+ };
+ renderImpressionStats(props);
+
+ // Loaded content + DISCOVERY_STREAM_SPOC_IMPRESSION + TOP_SITES_SPONSORED_IMPRESSION_STATS + impression
+ assert.callCount(dispatch, 4);
+
+ const [action] = dispatch.getCall(2).args;
+ assert.equal(action.type, at.TOP_SITES_SPONSORED_IMPRESSION_STATS);
+ assert.deepEqual(action.data, {
+ type: "impression",
+ tile_id: 1,
+ source: "newtab",
+ advertiser: "test advertiser",
+ position: 1,
+ });
+ });
+ it("should send an impression when the wrapped item transiting from invisible to visible", () => {
+ const dispatch = sinon.spy();
+ const props = {
+ dispatch,
+ IntersectionObserver: buildIntersectionObserver(ZeroIntersectEntries),
+ };
+ const wrapper = renderImpressionStats(props);
+
+ // For the loaded content
+ assert.calledOnce(dispatch);
+
+ let [action] = dispatch.firstCall.args;
+ assert.equal(action.type, at.DISCOVERY_STREAM_LOADED_CONTENT);
+ assert.equal(action.data.source, SOURCE);
+ assert.deepEqual(action.data.tiles, [
+ { id: 1, pos: 0 },
+ { id: 2, pos: 1 },
+ { id: 3, pos: 2 },
+ ]);
+
+ dispatch.resetHistory();
+ wrapper.instance().impressionObserver.callback(FullIntersectEntries);
+
+ // For the impression
+ assert.calledOnce(dispatch);
+
+ [action] = dispatch.firstCall.args;
+ assert.equal(action.type, at.DISCOVERY_STREAM_IMPRESSION_STATS);
+ assert.deepEqual(action.data.tiles, [
+ { id: 1, pos: 0, type: "organic" },
+ { id: 2, pos: 1, type: "organic" },
+ { id: 3, pos: 2, type: "organic" },
+ ]);
+ });
+ it("should remove visibility change listener when the wrapper is removed", () => {
+ const props = {
+ dispatch: sinon.spy(),
+ document: {
+ visibilityState: "hidden",
+ addEventListener: sinon.spy(),
+ removeEventListener: sinon.spy(),
+ },
+ IntersectionObserver,
+ };
+
+ const wrapper = renderImpressionStats(props);
+ assert.calledWith(props.document.addEventListener, "visibilitychange");
+ const [, listener] = props.document.addEventListener.firstCall.args;
+
+ wrapper.unmount();
+ assert.calledWith(
+ props.document.removeEventListener,
+ "visibilitychange",
+ listener
+ );
+ });
+ it("should unobserve the intersection observer when the wrapper is removed", () => {
+ const IntersectionObserver =
+ buildIntersectionObserver(ZeroIntersectEntries);
+ const spy = sinon.spy(IntersectionObserver.prototype, "unobserve");
+ const props = { dispatch: sinon.spy(), IntersectionObserver };
+
+ const wrapper = renderImpressionStats(props);
+ wrapper.unmount();
+
+ assert.calledOnce(spy);
+ });
+ it("should only send the latest impression on a visibility change", () => {
+ const listeners = new Set();
+ const props = {
+ dispatch: sinon.spy(),
+ document: {
+ visibilityState: "hidden",
+ addEventListener: (ev, cb) => listeners.add(cb),
+ removeEventListener: (ev, cb) => listeners.delete(cb),
+ },
+ };
+
+ const wrapper = renderImpressionStats(props);
+
+ // Update twice
+ wrapper.setProps({ ...props, ...{ rows: [{ id: 123, pos: 4 }] } });
+ wrapper.setProps({ ...props, ...{ rows: [{ id: 2432, pos: 5 }] } });
+
+ assert.notCalled(props.dispatch);
+
+ // Simulate listeners getting called
+ props.document.visibilityState = "visible";
+ listeners.forEach(l => l());
+
+ // Make sure we only sent the latest event
+ assert.calledTwice(props.dispatch);
+ const [action] = props.dispatch.firstCall.args;
+ assert.deepEqual(action.data.tiles, [{ id: 2432, pos: 5 }]);
+ });
+});