diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 09:22:09 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 09:22:09 +0000 |
commit | 43a97878ce14b72f0981164f87f2e35e14151312 (patch) | |
tree | 620249daf56c0258faa40cbdcf9cfba06de2a846 /browser/components/newtab/test/unit/content-src/lib | |
parent | Initial commit. (diff) | |
download | firefox-upstream.tar.xz firefox-upstream.zip |
Adding upstream version 110.0.1.upstream/110.0.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'browser/components/newtab/test/unit/content-src/lib')
5 files changed, 1139 insertions, 0 deletions
diff --git a/browser/components/newtab/test/unit/content-src/lib/detect-user-session-start.test.js b/browser/components/newtab/test/unit/content-src/lib/detect-user-session-start.test.js new file mode 100644 index 0000000000..5a7fad7cc0 --- /dev/null +++ b/browser/components/newtab/test/unit/content-src/lib/detect-user-session-start.test.js @@ -0,0 +1,120 @@ +import { + actionCreators as ac, + actionTypes as at, +} from "common/Actions.sys.mjs"; +import { DetectUserSessionStart } from "content-src/lib/detect-user-session-start"; + +describe("detectUserSessionStart", () => { + let store; + class PerfService { + getMostRecentAbsMarkStartByName() { + return 1234; + } + mark() {} + } + + beforeEach(() => { + store = { dispatch: () => {} }; + }); + describe("#sendEventOrAddListener", () => { + it("should call ._sendEvent immediately if the document is visible", () => { + const mockDocument = { visibilityState: "visible" }; + const instance = new DetectUserSessionStart(store, { + document: mockDocument, + }); + sinon.stub(instance, "_sendEvent"); + + instance.sendEventOrAddListener(); + + assert.calledOnce(instance._sendEvent); + }); + it("should add an event listener on visibility changes the document is not visible", () => { + const mockDocument = { + visibilityState: "hidden", + addEventListener: sinon.spy(), + }; + const instance = new DetectUserSessionStart(store, { + document: mockDocument, + }); + sinon.stub(instance, "_sendEvent"); + + instance.sendEventOrAddListener(); + + assert.notCalled(instance._sendEvent); + assert.calledWith( + mockDocument.addEventListener, + "visibilitychange", + instance._onVisibilityChange + ); + }); + }); + describe("#_sendEvent", () => { + it("should dispatch an action with the SAVE_SESSION_PERF_DATA", () => { + const dispatch = sinon.spy(store, "dispatch"); + const instance = new DetectUserSessionStart(store); + + instance._sendEvent(); + + assert.calledWith( + dispatch, + ac.AlsoToMain({ + type: at.SAVE_SESSION_PERF_DATA, + data: { visibility_event_rcvd_ts: sinon.match.number }, + }) + ); + }); + + it("shouldn't send a message if getMostRecentAbsMarkStartByName throws", () => { + let perfService = new PerfService(); + sinon.stub(perfService, "getMostRecentAbsMarkStartByName").throws(); + const dispatch = sinon.spy(store, "dispatch"); + const instance = new DetectUserSessionStart(store, { perfService }); + + instance._sendEvent(); + + assert.notCalled(dispatch); + }); + + it('should call perfService.mark("visibility_event_rcvd_ts")', () => { + let perfService = new PerfService(); + sinon.stub(perfService, "mark"); + const instance = new DetectUserSessionStart(store, { perfService }); + + instance._sendEvent(); + + assert.calledWith(perfService.mark, "visibility_event_rcvd_ts"); + }); + }); + + describe("_onVisibilityChange", () => { + it("should not send an event if visiblity is not visible", () => { + const instance = new DetectUserSessionStart(store, { + document: { visibilityState: "hidden" }, + }); + sinon.stub(instance, "_sendEvent"); + + instance._onVisibilityChange(); + + assert.notCalled(instance._sendEvent); + }); + it("should send an event and remove the event listener if visibility is visible", () => { + const mockDocument = { + visibilityState: "visible", + removeEventListener: sinon.spy(), + }; + const instance = new DetectUserSessionStart(store, { + document: mockDocument, + }); + sinon.stub(instance, "_sendEvent"); + + instance._onVisibilityChange(); + + assert.calledOnce(instance._sendEvent); + assert.calledWith( + mockDocument.removeEventListener, + "visibilitychange", + instance._onVisibilityChange + ); + }); + }); +}); diff --git a/browser/components/newtab/test/unit/content-src/lib/init-store.test.js b/browser/components/newtab/test/unit/content-src/lib/init-store.test.js new file mode 100644 index 0000000000..5ce92d2192 --- /dev/null +++ b/browser/components/newtab/test/unit/content-src/lib/init-store.test.js @@ -0,0 +1,207 @@ +import { + actionCreators as ac, + actionTypes as at, +} from "common/Actions.sys.mjs"; +import { addNumberReducer, GlobalOverrider } from "test/unit/utils"; +import { + EARLY_QUEUED_ACTIONS, + INCOMING_MESSAGE_NAME, + initStore, + MERGE_STORE_ACTION, + OUTGOING_MESSAGE_NAME, + queueEarlyMessageMiddleware, + rehydrationMiddleware, +} from "content-src/lib/init-store"; + +describe("initStore", () => { + let globals; + let store; + beforeEach(() => { + globals = new GlobalOverrider(); + globals.set("RPMSendAsyncMessage", globals.sandbox.spy()); + globals.set("RPMAddMessageListener", globals.sandbox.spy()); + store = initStore({ number: addNumberReducer }); + }); + afterEach(() => globals.restore()); + it("should create a store with the provided reducers", () => { + assert.ok(store); + assert.property(store.getState(), "number"); + }); + it("should add a listener that dispatches actions", () => { + assert.calledWith(global.RPMAddMessageListener, INCOMING_MESSAGE_NAME); + const [, listener] = global.RPMAddMessageListener.firstCall.args; + globals.sandbox.spy(store, "dispatch"); + const message = { name: INCOMING_MESSAGE_NAME, data: { type: "FOO" } }; + + listener(message); + + assert.calledWith(store.dispatch, message.data); + }); + it("should not throw if RPMAddMessageListener is not defined", () => { + // Note: this is being set/restored by GlobalOverrider + delete global.RPMAddMessageListener; + + assert.doesNotThrow(() => initStore({ number: addNumberReducer })); + }); + it("should log errors from failed messages", () => { + const [, callback] = global.RPMAddMessageListener.firstCall.args; + globals.sandbox.stub(global.console, "error"); + globals.sandbox.stub(store, "dispatch").throws(Error("failed")); + + const message = { + name: INCOMING_MESSAGE_NAME, + data: { type: MERGE_STORE_ACTION }, + }; + callback(message); + + assert.calledOnce(global.console.error); + }); + it("should replace the state if a MERGE_STORE_ACTION is dispatched", () => { + store.dispatch({ type: MERGE_STORE_ACTION, data: { number: 42 } }); + assert.deepEqual(store.getState(), { number: 42 }); + }); + it("should call .send and update the local store if an AlsoToMain action is dispatched", () => { + const subscriber = sinon.spy(); + const action = ac.AlsoToMain({ type: "FOO" }); + + store.subscribe(subscriber); + store.dispatch(action); + + assert.calledWith( + global.RPMSendAsyncMessage, + OUTGOING_MESSAGE_NAME, + action + ); + assert.calledOnce(subscriber); + }); + it("should call .send but not update the local store if an OnlyToMain action is dispatched", () => { + const subscriber = sinon.spy(); + const action = ac.OnlyToMain({ type: "FOO" }); + + store.subscribe(subscriber); + store.dispatch(action); + + assert.calledWith( + global.RPMSendAsyncMessage, + OUTGOING_MESSAGE_NAME, + action + ); + assert.notCalled(subscriber); + }); + it("should not send out other types of actions", () => { + store.dispatch({ type: "FOO" }); + assert.notCalled(global.RPMSendAsyncMessage); + }); + describe("rehydrationMiddleware", () => { + it("should allow NEW_TAB_STATE_REQUEST to go through", () => { + const action = ac.AlsoToMain({ type: at.NEW_TAB_STATE_REQUEST }); + const next = sinon.spy(); + rehydrationMiddleware(store)(next)(action); + assert.calledWith(next, action); + }); + it("should dispatch an additional NEW_TAB_STATE_REQUEST if INIT was received after a request", () => { + const requestAction = ac.AlsoToMain({ type: at.NEW_TAB_STATE_REQUEST }); + const next = sinon.spy(); + const dispatch = rehydrationMiddleware(store)(next); + + dispatch(requestAction); + next.resetHistory(); + dispatch({ type: at.INIT }); + + assert.calledWith(next, requestAction); + }); + it("should allow MERGE_STORE_ACTION to go through", () => { + const action = { type: MERGE_STORE_ACTION }; + const next = sinon.spy(); + rehydrationMiddleware(store)(next)(action); + assert.calledWith(next, action); + }); + it("should not allow actions from main to go through before MERGE_STORE_ACTION was received", () => { + const next = sinon.spy(); + const dispatch = rehydrationMiddleware(store)(next); + + dispatch(ac.BroadcastToContent({ type: "FOO" })); + dispatch(ac.AlsoToOneContent({ type: "FOO" }, 123)); + + assert.notCalled(next); + }); + it("should allow all local actions to go through", () => { + const action = { type: "FOO" }; + const next = sinon.spy(); + rehydrationMiddleware(store)(next)(action); + assert.calledWith(next, action); + }); + it("should allow actions from main to go through after MERGE_STORE_ACTION has been received", () => { + const next = sinon.spy(); + const dispatch = rehydrationMiddleware(store)(next); + + dispatch({ type: MERGE_STORE_ACTION }); + next.resetHistory(); + + const action = ac.AlsoToOneContent({ type: "FOO" }, 123); + dispatch(action); + assert.calledWith(next, action); + }); + it("should not let startup actions go through for the preloaded about:home document", () => { + globals.set("__FROM_STARTUP_CACHE__", true); + const next = sinon.spy(); + const dispatch = rehydrationMiddleware(store)(next); + const action = ac.BroadcastToContent( + { type: "FOO", meta: { isStartup: true } }, + 123 + ); + dispatch(action); + assert.notCalled(next); + }); + }); + describe("queueEarlyMessageMiddleware", () => { + it("should allow all local actions to go through", () => { + const action = { type: "FOO" }; + const next = sinon.spy(); + + queueEarlyMessageMiddleware(store)(next)(action); + + assert.calledWith(next, action); + }); + it("should allow action to main that does not belong to EARLY_QUEUED_ACTIONS to go through", () => { + const action = ac.AlsoToMain({ type: "FOO" }); + const next = sinon.spy(); + + queueEarlyMessageMiddleware(store)(next)(action); + + assert.calledWith(next, action); + }); + it(`should line up EARLY_QUEUED_ACTIONS only let them go through after it receives the action from main`, () => { + EARLY_QUEUED_ACTIONS.forEach(actionType => { + const testStore = initStore({ number: addNumberReducer }); + const next = sinon.spy(); + const dispatch = queueEarlyMessageMiddleware(testStore)(next); + const action = ac.AlsoToMain({ type: actionType }); + const fromMainAction = ac.AlsoToOneContent({ type: "FOO" }, 123); + + // Early actions should be added to the queue + dispatch(action); + dispatch(action); + + assert.notCalled(next); + assert.equal(testStore.getState.earlyActionQueue.length, 2); + next.resetHistory(); + + // Receiving action from main would empty the queue + dispatch(fromMainAction); + + assert.calledThrice(next); + assert.equal(next.firstCall.args[0], fromMainAction); + assert.equal(next.secondCall.args[0], action); + assert.equal(next.thirdCall.args[0], action); + assert.equal(testStore.getState.earlyActionQueue.length, 0); + next.resetHistory(); + + // New action should go through immediately + dispatch(action); + assert.calledOnce(next); + assert.calledWith(next, action); + }); + }); + }); +}); diff --git a/browser/components/newtab/test/unit/content-src/lib/perf-service.test.js b/browser/components/newtab/test/unit/content-src/lib/perf-service.test.js new file mode 100644 index 0000000000..9cabfb5029 --- /dev/null +++ b/browser/components/newtab/test/unit/content-src/lib/perf-service.test.js @@ -0,0 +1,89 @@ +/* globals assert, beforeEach, describe, it */ +import { _PerfService } from "content-src/lib/perf-service"; +import { FakePerformance } from "test/unit/utils.js"; + +let perfService; + +describe("_PerfService", () => { + let sandbox; + let fakePerfObj; + + beforeEach(() => { + sandbox = sinon.createSandbox(); + fakePerfObj = new FakePerformance(); + perfService = new _PerfService({ performanceObj: fakePerfObj }); + }); + + afterEach(() => { + sandbox.restore(); + }); + + describe("#absNow", () => { + it("should return a number > the time origin", () => { + const absNow = perfService.absNow(); + + assert.isAbove(absNow, perfService.timeOrigin); + }); + }); + describe("#getEntriesByName", () => { + it("should call getEntriesByName on the appropriate Window.performance", () => { + sandbox.spy(fakePerfObj, "getEntriesByName"); + + perfService.getEntriesByName("monkey", "mark"); + + assert.calledOnce(fakePerfObj.getEntriesByName); + assert.calledWithExactly(fakePerfObj.getEntriesByName, "monkey", "mark"); + }); + + it("should return entries with the given name", () => { + sandbox.spy(fakePerfObj, "getEntriesByName"); + perfService.mark("monkey"); + perfService.mark("dog"); + + let marks = perfService.getEntriesByName("monkey", "mark"); + + assert.isArray(marks); + assert.lengthOf(marks, 1); + assert.propertyVal(marks[0], "name", "monkey"); + }); + }); + + describe("#getMostRecentAbsMarkStartByName", () => { + it("should throw an error if there is no mark with the given name", () => { + function bogusGet() { + perfService.getMostRecentAbsMarkStartByName("rheeeet"); + } + + assert.throws(bogusGet, Error, /No marks with the name/); + }); + + it("should return the Number from the most recent mark with the given name + the time origin", () => { + perfService.mark("dog"); + perfService.mark("dog"); + + let absMarkStart = perfService.getMostRecentAbsMarkStartByName("dog"); + + // 2 because we want the result of the 2nd call to mark, and an instance + // of FakePerformance just returns the number of time mark has been + // called. + assert.equal(absMarkStart - perfService.timeOrigin, 2); + }); + }); + + describe("#mark", () => { + it("should call the wrapped version of mark", () => { + sandbox.spy(fakePerfObj, "mark"); + + perfService.mark("monkey"); + + assert.calledOnce(fakePerfObj.mark); + assert.calledWithExactly(fakePerfObj.mark, "monkey"); + }); + }); + + describe("#timeOrigin", () => { + it("should get the origin of the wrapped performance object", () => { + assert.equal(perfService.timeOrigin, fakePerfObj.timeOrigin); + }); + }); +}); diff --git a/browser/components/newtab/test/unit/content-src/lib/screenshot-utils.test.js b/browser/components/newtab/test/unit/content-src/lib/screenshot-utils.test.js new file mode 100644 index 0000000000..ef7e7cf5f6 --- /dev/null +++ b/browser/components/newtab/test/unit/content-src/lib/screenshot-utils.test.js @@ -0,0 +1,147 @@ +import { GlobalOverrider } from "test/unit/utils"; +import { ScreenshotUtils } from "content-src/lib/screenshot-utils"; + +const DEFAULT_BLOB_URL = "blob://test"; + +describe("ScreenshotUtils", () => { + let globals; + let url; + beforeEach(() => { + globals = new GlobalOverrider(); + url = { + createObjectURL: globals.sandbox.stub().returns(DEFAULT_BLOB_URL), + revokeObjectURL: globals.sandbox.spy(), + }; + globals.set("URL", url); + }); + afterEach(() => globals.restore()); + describe("#createLocalImageObject", () => { + it("should return null if no remoteImage is supplied", () => { + let localImageObject = ScreenshotUtils.createLocalImageObject(null); + + assert.notCalled(url.createObjectURL); + assert.equal(localImageObject, null); + }); + it("should create a local image object with the correct properties if remoteImage is a blob", () => { + let localImageObject = ScreenshotUtils.createLocalImageObject({ + path: "/path1", + data: new Blob([0]), + }); + + assert.calledOnce(url.createObjectURL); + assert.deepEqual(localImageObject, { + path: "/path1", + url: DEFAULT_BLOB_URL, + }); + }); + it("should create a local image object with the correct properties if remoteImage is a normal image", () => { + const imageUrl = "https://test-url"; + let localImageObject = ScreenshotUtils.createLocalImageObject(imageUrl); + + assert.notCalled(url.createObjectURL); + assert.deepEqual(localImageObject, { url: imageUrl }); + }); + }); + describe("#maybeRevokeBlobObjectURL", () => { + // Note that we should also ensure that all the tests for #isBlob are green. + it("should call revokeObjectURL if image is a blob", () => { + ScreenshotUtils.maybeRevokeBlobObjectURL({ + path: "/path1", + url: "blob://test", + }); + + assert.calledOnce(url.revokeObjectURL); + }); + it("should not call revokeObjectURL if image is not a blob", () => { + ScreenshotUtils.maybeRevokeBlobObjectURL({ url: "https://test-url" }); + + assert.notCalled(url.revokeObjectURL); + }); + }); + describe("#isRemoteImageLocal", () => { + it("should return true if both propsImage and stateImage are not present", () => { + assert.isTrue(ScreenshotUtils.isRemoteImageLocal(null, null)); + }); + it("should return false if propsImage is present and stateImage is not present", () => { + assert.isFalse(ScreenshotUtils.isRemoteImageLocal(null, {})); + }); + it("should return false if propsImage is not present and stateImage is present", () => { + assert.isFalse(ScreenshotUtils.isRemoteImageLocal({}, null)); + }); + it("should return true if both propsImage and stateImage are equal blobs", () => { + const blobPath = "/test-blob-path/test.png"; + assert.isTrue( + ScreenshotUtils.isRemoteImageLocal( + { path: blobPath, url: "blob://test" }, // state + { path: blobPath, data: new Blob([0]) } // props + ) + ); + }); + it("should return false if both propsImage and stateImage are different blobs", () => { + assert.isFalse( + ScreenshotUtils.isRemoteImageLocal( + { path: "/path1", url: "blob://test" }, // state + { path: "/path2", data: new Blob([0]) } // props + ) + ); + }); + it("should return true if both propsImage and stateImage are equal normal images", () => { + assert.isTrue( + ScreenshotUtils.isRemoteImageLocal( + { url: "test url" }, // state + "test url" // props + ) + ); + }); + it("should return false if both propsImage and stateImage are different normal images", () => { + assert.isFalse( + ScreenshotUtils.isRemoteImageLocal( + { url: "test url 1" }, // state + "test url 2" // props + ) + ); + }); + it("should return false if both propsImage and stateImage are different type of images", () => { + assert.isFalse( + ScreenshotUtils.isRemoteImageLocal( + { path: "/path1", url: "blob://test" }, // state + "test url 2" // props + ) + ); + assert.isFalse( + ScreenshotUtils.isRemoteImageLocal( + { url: "https://test-url" }, // state + { path: "/path1", data: new Blob([0]) } // props + ) + ); + }); + }); + describe("#isBlob", () => { + let state = { + blobImage: { path: "/test", url: "blob://test" }, + normalImage: { url: "https://test-url" }, + }; + let props = { + blobImage: { path: "/test", data: new Blob([0]) }, + normalImage: "https://test-url", + }; + it("should return false if image is null", () => { + assert.isFalse(ScreenshotUtils.isBlob(true, null)); + assert.isFalse(ScreenshotUtils.isBlob(false, null)); + }); + it("should return true if image is a blob and type matches", () => { + assert.isTrue(ScreenshotUtils.isBlob(true, state.blobImage)); + assert.isTrue(ScreenshotUtils.isBlob(false, props.blobImage)); + }); + it("should return false if image is not a blob and type matches", () => { + assert.isFalse(ScreenshotUtils.isBlob(true, state.normalImage)); + assert.isFalse(ScreenshotUtils.isBlob(false, props.normalImage)); + }); + it("should return false if type does not match", () => { + assert.isFalse(ScreenshotUtils.isBlob(false, state.blobImage)); + assert.isFalse(ScreenshotUtils.isBlob(false, state.normalImage)); + assert.isFalse(ScreenshotUtils.isBlob(true, props.blobImage)); + assert.isFalse(ScreenshotUtils.isBlob(true, props.normalImage)); + }); + }); +}); diff --git a/browser/components/newtab/test/unit/content-src/lib/selectLayoutRender.test.js b/browser/components/newtab/test/unit/content-src/lib/selectLayoutRender.test.js new file mode 100644 index 0000000000..70886c1b4b --- /dev/null +++ b/browser/components/newtab/test/unit/content-src/lib/selectLayoutRender.test.js @@ -0,0 +1,576 @@ +import { combineReducers, createStore } from "redux"; +import { actionTypes as at } from "common/Actions.sys.mjs"; +import { GlobalOverrider } from "test/unit/utils"; +import { reducers } from "common/Reducers.jsm"; +import { selectLayoutRender } from "content-src/lib/selectLayoutRender"; +const FAKE_LAYOUT = [ + { + width: 3, + components: [ + { type: "foo", feed: { url: "foo.com" }, properties: { items: 2 } }, + ], + }, +]; +const FAKE_FEEDS = { + "foo.com": { data: { recommendations: [{ id: "foo" }, { id: "bar" }] } }, +}; + +describe("selectLayoutRender", () => { + let store; + let globals; + + beforeEach(() => { + globals = new GlobalOverrider(); + store = createStore(combineReducers(reducers)); + }); + + afterEach(() => { + globals.restore(); + }); + + it("should return an empty array given initial state", () => { + const { layoutRender } = selectLayoutRender({ + state: store.getState().DiscoveryStream, + prefs: {}, + rollCache: [], + }); + assert.deepEqual(layoutRender, []); + }); + + it("should add .data property from feeds to each compontent in .layout", () => { + store.dispatch({ + type: at.DISCOVERY_STREAM_LAYOUT_UPDATE, + data: { layout: FAKE_LAYOUT }, + }); + store.dispatch({ + type: at.DISCOVERY_STREAM_FEED_UPDATE, + data: { feed: FAKE_FEEDS["foo.com"], url: "foo.com" }, + }); + store.dispatch({ type: at.DISCOVERY_STREAM_FEEDS_UPDATE }); + + const { layoutRender } = selectLayoutRender({ + state: store.getState().DiscoveryStream, + }); + + assert.lengthOf(layoutRender, 1); + assert.propertyVal(layoutRender[0], "width", 3); + assert.deepEqual(layoutRender[0].components[0], { + type: "foo", + feed: { url: "foo.com" }, + properties: { items: 2 }, + data: { + recommendations: [ + { id: "foo", pos: 0 }, + { id: "bar", pos: 1 }, + ], + }, + }); + }); + + it("should return layout with placeholder data if feed doesn't have data", () => { + store.dispatch({ + type: at.DISCOVERY_STREAM_LAYOUT_UPDATE, + data: { layout: FAKE_LAYOUT }, + }); + store.dispatch({ type: at.DISCOVERY_STREAM_FEEDS_UPDATE }); + + const { layoutRender } = selectLayoutRender({ + state: store.getState().DiscoveryStream, + }); + + assert.lengthOf(layoutRender, 1); + assert.propertyVal(layoutRender[0], "width", 3); + assert.deepEqual(layoutRender[0].components[0].data.recommendations, [ + { placeholder: true }, + { placeholder: true }, + ]); + }); + + it("should return layout with empty spocs data if feed isn't defined but spocs is", () => { + const fakeLayout = [ + { + width: 3, + components: [{ type: "foo", spocs: { positions: [{ index: 2 }] } }], + }, + ]; + store.dispatch({ + type: at.DISCOVERY_STREAM_LAYOUT_UPDATE, + data: { layout: fakeLayout }, + }); + store.dispatch({ type: at.DISCOVERY_STREAM_FEEDS_UPDATE }); + + const { layoutRender } = selectLayoutRender({ + state: store.getState().DiscoveryStream, + }); + + assert.lengthOf(layoutRender, 1); + assert.propertyVal(layoutRender[0], "width", 3); + assert.deepEqual(layoutRender[0].components[0].data.spocs, []); + }); + + it("should return layout with spocs data if feed isn't defined but spocs is", () => { + const fakeLayout = [ + { + width: 3, + components: [{ type: "foo", spocs: { positions: [{ index: 0 }] } }], + }, + ]; + store.dispatch({ + type: at.DISCOVERY_STREAM_LAYOUT_UPDATE, + data: { layout: fakeLayout }, + }); + store.dispatch({ type: at.DISCOVERY_STREAM_FEEDS_UPDATE }); + store.dispatch({ + type: at.DISCOVERY_STREAM_SPOCS_UPDATE, + data: { + lastUpdated: 0, + spocs: { + spocs: { + items: [{ id: 1 }, { id: 2 }, { id: 3 }], + }, + }, + }, + }); + + const { layoutRender } = selectLayoutRender({ + state: store.getState().DiscoveryStream, + }); + + assert.lengthOf(layoutRender, 1); + assert.propertyVal(layoutRender[0], "width", 3); + assert.deepEqual(layoutRender[0].components[0].data.spocs, [ + { id: 1, pos: 0 }, + { id: 2, pos: 1 }, + { id: 3, pos: 2 }, + ]); + }); + + it("should return layout with no spocs data if feed and spocs are unavailable", () => { + const fakeLayout = [ + { + width: 3, + components: [{ type: "foo", spocs: { positions: [{ index: 0 }] } }], + }, + ]; + store.dispatch({ + type: at.DISCOVERY_STREAM_LAYOUT_UPDATE, + data: { layout: fakeLayout }, + }); + store.dispatch({ type: at.DISCOVERY_STREAM_FEEDS_UPDATE }); + store.dispatch({ + type: at.DISCOVERY_STREAM_SPOCS_UPDATE, + data: { + lastUpdated: 0, + spocs: { + spocs: { + items: [], + }, + }, + }, + }); + + const { layoutRender } = selectLayoutRender({ + state: store.getState().DiscoveryStream, + }); + + assert.lengthOf(layoutRender, 1); + assert.propertyVal(layoutRender[0], "width", 3); + assert.equal(layoutRender[0].components[0].data.spocs.length, 0); + }); + + it("should return feed data offset by layout set prop", () => { + const fakeLayout = [ + { + width: 3, + components: [ + { type: "foo", properties: { offset: 1 }, feed: { url: "foo.com" } }, + ], + }, + ]; + store.dispatch({ + type: at.DISCOVERY_STREAM_LAYOUT_UPDATE, + data: { layout: fakeLayout }, + }); + store.dispatch({ + type: at.DISCOVERY_STREAM_FEED_UPDATE, + data: { feed: FAKE_FEEDS["foo.com"], url: "foo.com" }, + }); + store.dispatch({ type: at.DISCOVERY_STREAM_FEEDS_UPDATE }); + + const { layoutRender } = selectLayoutRender({ + state: store.getState().DiscoveryStream, + }); + + assert.deepEqual(layoutRender[0].components[0].data, { + recommendations: [{ id: "bar" }], + }); + }); + + it("should return spoc result when there are more positions than spocs", () => { + const fakeSpocConfig = { + positions: [{ index: 0 }, { index: 1 }, { index: 2 }], + }; + const fakeLayout = [ + { + width: 3, + components: [ + { type: "foo", feed: { url: "foo.com" }, spocs: fakeSpocConfig }, + ], + }, + ]; + const fakeSpocsData = { + lastUpdated: 0, + spocs: { spocs: { items: ["fooSpoc", "barSpoc"] } }, + }; + + store.dispatch({ + type: at.DISCOVERY_STREAM_LAYOUT_UPDATE, + data: { layout: fakeLayout }, + }); + store.dispatch({ + type: at.DISCOVERY_STREAM_FEED_UPDATE, + data: { feed: FAKE_FEEDS["foo.com"], url: "foo.com" }, + }); + store.dispatch({ type: at.DISCOVERY_STREAM_FEEDS_UPDATE }); + store.dispatch({ + type: at.DISCOVERY_STREAM_SPOCS_UPDATE, + data: fakeSpocsData, + }); + + const { layoutRender } = selectLayoutRender({ + state: store.getState().DiscoveryStream, + }); + + assert.lengthOf(layoutRender, 1); + assert.deepEqual( + layoutRender[0].components[0].data.recommendations[0], + "fooSpoc" + ); + assert.deepEqual( + layoutRender[0].components[0].data.recommendations[1], + "barSpoc" + ); + assert.deepEqual(layoutRender[0].components[0].data.recommendations[2], { + id: "foo", + }); + assert.deepEqual(layoutRender[0].components[0].data.recommendations[3], { + id: "bar", + }); + }); + + it("should return a layout with feeds of items length with positions", () => { + const fakeLayout = [ + { + width: 3, + components: [ + { type: "foo", properties: { items: 3 }, feed: { url: "foo.com" } }, + ], + }, + ]; + const fakeRecommendations = [ + { name: "item1" }, + { name: "item2" }, + { name: "item3" }, + { name: "item4" }, + ]; + const fakeFeeds = { + "foo.com": { data: { recommendations: fakeRecommendations } }, + }; + store.dispatch({ + type: at.DISCOVERY_STREAM_LAYOUT_UPDATE, + data: { layout: fakeLayout }, + }); + store.dispatch({ + type: at.DISCOVERY_STREAM_FEED_UPDATE, + data: { feed: fakeFeeds["foo.com"], url: "foo.com" }, + }); + store.dispatch({ type: at.DISCOVERY_STREAM_FEEDS_UPDATE }); + + const { layoutRender } = selectLayoutRender({ + state: store.getState().DiscoveryStream, + }); + + const { recommendations } = layoutRender[0].components[0].data; + assert.equal(recommendations.length, 4); + assert.equal(recommendations[0].pos, 0); + assert.equal(recommendations[1].pos, 1); + assert.equal(recommendations[2].pos, 2); + assert.equal(recommendations[3].pos, undefined); + }); + it("should stop rendering feeds if we hit one that's not ready", () => { + const fakeLayout = [ + { + width: 3, + components: [ + { type: "foo1" }, + { type: "foo2", properties: { items: 3 }, feed: { url: "foo2.com" } }, + { type: "foo3", properties: { items: 3 }, feed: { url: "foo3.com" } }, + { type: "foo4", properties: { items: 3 }, feed: { url: "foo4.com" } }, + { type: "foo5" }, + ], + }, + ]; + store.dispatch({ + type: at.DISCOVERY_STREAM_LAYOUT_UPDATE, + data: { layout: fakeLayout }, + }); + store.dispatch({ + type: at.DISCOVERY_STREAM_FEED_UPDATE, + data: { feed: { data: { recommendations: [] } }, url: "foo2.com" }, + }); + + const { layoutRender } = selectLayoutRender({ + state: store.getState().DiscoveryStream, + }); + + assert.equal(layoutRender[0].components[0].type, "foo1"); + assert.equal(layoutRender[0].components[1].type, "foo2"); + assert.isTrue( + layoutRender[0].components[2].data.recommendations[0].placeholder + ); + assert.lengthOf(layoutRender[0].components, 3); + assert.isUndefined(layoutRender[0].components[3]); + }); + it("should render everything if everything is ready", () => { + const fakeLayout = [ + { + width: 3, + components: [ + { type: "foo1" }, + { type: "foo2", properties: { items: 3 }, feed: { url: "foo2.com" } }, + { type: "foo3", properties: { items: 3 }, feed: { url: "foo3.com" } }, + { type: "foo4", properties: { items: 3 }, feed: { url: "foo4.com" } }, + { type: "foo5" }, + ], + }, + ]; + store.dispatch({ + type: at.DISCOVERY_STREAM_LAYOUT_UPDATE, + data: { layout: fakeLayout }, + }); + store.dispatch({ + type: at.DISCOVERY_STREAM_FEED_UPDATE, + data: { feed: { data: { recommendations: [] } }, url: "foo2.com" }, + }); + store.dispatch({ + type: at.DISCOVERY_STREAM_FEED_UPDATE, + data: { feed: { data: { recommendations: [] } }, url: "foo3.com" }, + }); + store.dispatch({ + type: at.DISCOVERY_STREAM_FEED_UPDATE, + data: { feed: { data: { recommendations: [] } }, url: "foo4.com" }, + }); + + const { layoutRender } = selectLayoutRender({ + state: store.getState().DiscoveryStream, + }); + + assert.equal(layoutRender[0].components[0].type, "foo1"); + assert.equal(layoutRender[0].components[1].type, "foo2"); + assert.equal(layoutRender[0].components[2].type, "foo3"); + assert.equal(layoutRender[0].components[3].type, "foo4"); + assert.equal(layoutRender[0].components[4].type, "foo5"); + }); + it("should stop rendering feeds if we hit a not ready spoc", () => { + const fakeLayout = [ + { + width: 3, + components: [ + { type: "foo1" }, + { type: "foo2", properties: { items: 3 }, feed: { url: "foo2.com" } }, + { + type: "foo3", + properties: { items: 3 }, + feed: { url: "foo3.com" }, + spocs: { positions: [{ index: 0 }] }, + }, + { type: "foo4", properties: { items: 3 }, feed: { url: "foo4.com" } }, + { type: "foo5" }, + ], + }, + ]; + store.dispatch({ + type: at.DISCOVERY_STREAM_LAYOUT_UPDATE, + data: { layout: fakeLayout }, + }); + store.dispatch({ + type: at.DISCOVERY_STREAM_FEED_UPDATE, + data: { feed: { data: { recommendations: [] } }, url: "foo2.com" }, + }); + store.dispatch({ + type: at.DISCOVERY_STREAM_FEED_UPDATE, + data: { feed: { data: { recommendations: [] } }, url: "foo3.com" }, + }); + store.dispatch({ + type: at.DISCOVERY_STREAM_FEED_UPDATE, + data: { feed: { data: { recommendations: [] } }, url: "foo4.com" }, + }); + + const { layoutRender } = selectLayoutRender({ + state: store.getState().DiscoveryStream, + }); + + assert.equal(layoutRender[0].components[0].type, "foo1"); + assert.equal(layoutRender[0].components[1].type, "foo2"); + assert.deepEqual(layoutRender[0].components[2].data.recommendations, [ + { placeholder: true }, + { placeholder: true }, + { placeholder: true }, + ]); + }); + it("should not render a spoc if there are no available spocs", () => { + const fakeLayout = [ + { + width: 3, + components: [ + { type: "foo1" }, + { type: "foo2", properties: { items: 3 }, feed: { url: "foo2.com" } }, + { + type: "foo3", + properties: { items: 3 }, + feed: { url: "foo3.com" }, + spocs: { positions: [{ index: 0 }] }, + }, + { type: "foo4", properties: { items: 3 }, feed: { url: "foo4.com" } }, + { type: "foo5" }, + ], + }, + ]; + const fakeSpocsData = { lastUpdated: 0, spocs: { spocs: [] } }; + store.dispatch({ + type: at.DISCOVERY_STREAM_LAYOUT_UPDATE, + data: { layout: fakeLayout }, + }); + store.dispatch({ + type: at.DISCOVERY_STREAM_FEED_UPDATE, + data: { feed: { data: { recommendations: [] } }, url: "foo2.com" }, + }); + store.dispatch({ + type: at.DISCOVERY_STREAM_FEED_UPDATE, + data: { + feed: { data: { recommendations: [{ name: "rec" }] } }, + url: "foo3.com", + }, + }); + store.dispatch({ + type: at.DISCOVERY_STREAM_FEED_UPDATE, + data: { feed: { data: { recommendations: [] } }, url: "foo4.com" }, + }); + store.dispatch({ + type: at.DISCOVERY_STREAM_SPOCS_UPDATE, + data: fakeSpocsData, + }); + + const { layoutRender } = selectLayoutRender({ + state: store.getState().DiscoveryStream, + }); + + assert.deepEqual(layoutRender[0].components[2].data.recommendations[0], { + name: "rec", + pos: 0, + }); + }); + it("should not render a row if no components exist after filter in that row", () => { + const fakeLayout = [ + { + width: 3, + components: [{ type: "TopSites" }], + }, + { + width: 3, + components: [{ type: "Message" }], + }, + ]; + store.dispatch({ + type: at.DISCOVERY_STREAM_LAYOUT_UPDATE, + data: { layout: fakeLayout }, + }); + + const { layoutRender } = selectLayoutRender({ + state: store.getState().DiscoveryStream, + prefs: { "feeds.topsites": true }, + }); + + assert.equal(layoutRender[0].components[0].type, "TopSites"); + assert.equal(layoutRender[1], undefined); + }); + it("should not render a component if filtered", () => { + const fakeLayout = [ + { + width: 3, + components: [{ type: "Message" }, { type: "TopSites" }], + }, + ]; + store.dispatch({ + type: at.DISCOVERY_STREAM_LAYOUT_UPDATE, + data: { layout: fakeLayout }, + }); + + const { layoutRender } = selectLayoutRender({ + state: store.getState().DiscoveryStream, + prefs: { "feeds.topsites": true }, + }); + + assert.equal(layoutRender[0].components[0].type, "TopSites"); + assert.equal(layoutRender[0].components[1], undefined); + }); + it("should skip rendering a spoc in position if that spoc is blocked for that session", () => { + const fakeLayout = [ + { + width: 3, + components: [ + { + type: "foo1", + properties: { items: 3 }, + feed: { url: "foo1.com" }, + spocs: { positions: [{ index: 0 }] }, + }, + ], + }, + ]; + const fakeSpocsData = { + lastUpdated: 0, + spocs: { + spocs: { items: [{ name: "spoc", url: "https://foo.com" }] }, + }, + }; + store.dispatch({ + type: at.DISCOVERY_STREAM_LAYOUT_UPDATE, + data: { layout: fakeLayout }, + }); + store.dispatch({ + type: at.DISCOVERY_STREAM_FEED_UPDATE, + data: { + feed: { data: { recommendations: [{ name: "rec" }] } }, + url: "foo1.com", + }, + }); + store.dispatch({ + type: at.DISCOVERY_STREAM_SPOCS_UPDATE, + data: fakeSpocsData, + }); + + const { layoutRender: layout1 } = selectLayoutRender({ + state: store.getState().DiscoveryStream, + }); + + store.dispatch({ + type: at.DISCOVERY_STREAM_SPOC_BLOCKED, + data: { url: "https://foo.com" }, + }); + + const { layoutRender: layout2 } = selectLayoutRender({ + state: store.getState().DiscoveryStream, + }); + + assert.deepEqual(layout1[0].components[0].data.recommendations[0], { + name: "spoc", + url: "https://foo.com", + pos: 0, + }); + assert.deepEqual(layout2[0].components[0].data.recommendations[0], { + name: "rec", + pos: 0, + }); + }); +}); |