From 6bf0a5cb5034a7e684dcc3500e841785237ce2dd Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sun, 7 Apr 2024 19:32:43 +0200 Subject: Adding upstream version 1:115.7.0. Signed-off-by: Daniel Baumann --- .../ImpressionStats.test.jsx | 278 +++++++++++++++++++++ 1 file changed, 278 insertions(+) create mode 100644 browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/ImpressionStats.test.jsx (limited to 'browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/ImpressionStats.test.jsx') 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("", () => { + 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 = () =>
Inner Element
; + + function renderImpressionStats(props = {}) { + return shallow( + + + + ); + } + + it("should render props.children", () => { + const wrapper = renderImpressionStats(); + assert.ok(wrapper.contains()); + }); + 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 }]); + }); +}); -- cgit v1.2.3