import React from "react"; import * as useFeedLoaderModule from "@applicaster/zapp-react-native-utils/reactHooks/feed/useFeedLoader"; import * as useFeedRefreshModule from "@applicaster/zapp-react-native-utils/reactHooks/feed/useFeedRefresh"; import { favoritesListener } from "@applicaster/zapp-react-native-bridge/Favorites"; import { UrlFeedResolver } from "../resolvers/UrlFeedResolver"; import { renderWithProviders } from "@applicaster/zapp-react-native-utils/testUtils"; import * as ZappPipesModule from "@applicaster/zapp-pipes-v2-client"; const mockSubscribeForKeyChanges = jest.fn(); jest .spyOn(ZappPipesModule, "subscribeForKeyChanges") .mockImplementation(mockSubscribeForKeyChanges); jest.mock("@applicaster/zapp-react-native-utils/reactHooks/feed/useFeedLoader"); jest.mock( "@applicaster/zapp-react-native-utils/reactHooks/feed/useFeedRefresh" ); jest.mock("@applicaster/zapp-pipes-v2-client"); jest.mock("@applicaster/zapp-react-native-bridge/Favorites", () => ({ favoritesListener: { on: jest.fn(), removeHandler: jest.fn(), }, })); jest.mock("../utils", () => ({ isVerticalListOrGrid: jest.fn( (component) => component?.type === "vertical_list" || component?.type === "vertical_grid" ), })); const componentRequiredKeys = { rules: { item_limit: 10 } }; describe("UrlFeedResolver", () => { const mockChildren = jest.fn(() =>
); const mockReloadData = jest.fn(); const mockLoadNext = jest.fn(); const mockAddDataSourceListener = jest.fn().mockReturnValue(jest.fn()); beforeEach(() => { jest.clearAllMocks(); (useFeedLoaderModule.useFeedLoader as jest.Mock).mockReturnValue({ reloadData: mockReloadData, loadNext: mockLoadNext, error: null, loading: false, url: "test-url", data: { entry: [{ id: "1" }] }, }); }); it("should use feedUrl directly when provided", () => { const props = { feedUrl: "https://example.com/feed", component: { ...componentRequiredKeys } as any, children: mockChildren, riverId: "test-river", }; renderWithProviders(); expect(useFeedLoaderModule.useFeedLoader).toHaveBeenCalledWith({ feedUrl: "https://example.com/feed", pipesOptions: expect.any(Object), }); }); it("should use component.data.source when no feedUrl is provided", () => { const props = { component: { data: { source: "https://example.com/source", }, ...componentRequiredKeys, } as any, children: mockChildren, riverId: "test-river", }; renderWithProviders(); expect(useFeedLoaderModule.useFeedLoader).toHaveBeenCalledWith({ feedUrl: "https://example.com/source", pipesOptions: expect.any(Object), }); }); it("should handle FAVOURITES type correctly", () => { const props = { component: { data: { source: "favorites-source", type: "FAVOURITES", }, ...componentRequiredKeys, } as any, children: mockChildren, riverId: "test-river", }; renderWithProviders(); expect(useFeedLoaderModule.useFeedLoader).toHaveBeenCalledWith({ feedUrl: "favorites-source", pipesOptions: expect.objectContaining({ clearCache: true, loadLocalFavorites: true, }), }); expect(favoritesListener.on).toHaveBeenCalledWith( "FAVORITES_CHANGED", mockReloadData ); }); it("should handle APPLICASTER_COLLECTION type correctly", () => { const props = { component: { data: { source: "collection-id", type: "APPLICASTER_COLLECTION", }, ...componentRequiredKeys, } as any, children: mockChildren, riverId: "test-river", }; renderWithProviders(); expect(useFeedLoaderModule.useFeedLoader).toHaveBeenCalledWith({ feedUrl: "applicaster://fetchData?type=APPLICASTER_COLLECTION&collectionId=collection-id", pipesOptions: expect.any(Object), }); }); it("should handle APPLICASTER_CATEGORY type correctly", () => { const props = { component: { data: { source: "category-id", type: "APPLICASTER_CATEGORY", }, ...componentRequiredKeys, } as any, children: mockChildren, riverId: "test-river", plugins: [], }; renderWithProviders(); expect(useFeedLoaderModule.useFeedLoader).toHaveBeenCalledWith({ feedUrl: expect.stringContaining( "applicaster://fetchData?type=APPLICASTER_CATEGORY&categoryId=category-id" ), pipesOptions: expect.any(Object), }); }); it("should set up URL context key changes listener", () => { const props = { feedUrl: "https://example.com/feed", component: { ...componentRequiredKeys } as any, children: mockChildren, riverId: "test-river", }; renderWithProviders(); expect(mockSubscribeForKeyChanges.mock.calls[0][0]).toMatchObject({ url: "https://example.com/feed", callback: mockReloadData, }); }); it("should set up pipesv2 URL listener from plugin if available", () => { const props = { feedUrl: "pipesv2://test-plugin/endpoint", component: { ...componentRequiredKeys } as any, children: mockChildren, riverId: "test-river", plugins: [ { identifier: "test-plugin", module: { addDataSourceListener: mockAddDataSourceListener, }, }, ] as any, }; renderWithProviders(); expect(mockAddDataSourceListener).toHaveBeenCalledWith(mockReloadData); expect(mockSubscribeForKeyChanges).not.toHaveBeenCalled(); }); it("should apply item limit from component rules", () => { const props = { feedUrl: "https://example.com/feed", component: { rules: { item_limit: "2", }, } as any, children: mockChildren, riverId: "test-river", }; (useFeedLoaderModule.useFeedLoader as jest.Mock).mockReturnValue({ reloadData: mockReloadData, loadNext: mockLoadNext, error: null, loading: false, url: "test-url", data: { entry: [{ id: "1" }, { id: "2" }, { id: "3" }] }, }); renderWithProviders(); expect(mockChildren).toHaveBeenCalledWith( expect.objectContaining({ zappPipesData: expect.objectContaining({ data: expect.objectContaining({ entry: [{ id: "1" }, { id: "2" }], }), }), }) ); }); it("should conditionally include loadNextData based on isLast and component type", () => { // Case 1: isLast=true, should not include loadNextData regardless of component type const props1 = { feedUrl: "https://example.com/feed", component: { type: "vertical_list", ...componentRequiredKeys, } as any, children: mockChildren, riverId: "test-river", isLast: true, }; renderWithProviders(); expect(mockChildren).toHaveBeenCalledWith( expect.objectContaining({ loadNextData: mockLoadNext, }) ); mockChildren.mockClear(); // Case 2: isLast=false, vertical_list component, should not include loadNextData const props2 = { feedUrl: "https://example.com/feed", component: { type: "vertical_list", ...componentRequiredKeys, } as any, children: mockChildren, riverId: "test-river", isLast: false, }; renderWithProviders(); expect(mockChildren).toHaveBeenCalledWith( expect.objectContaining({ loadNextData: undefined, }) ); mockChildren.mockClear(); // Case 3: isLast=false, non-vertical component, should include loadNextData const props3 = { feedUrl: "https://example.com/feed", component: { type: "horizontal_list", ...componentRequiredKeys, } as any, children: mockChildren, riverId: "test-river", isLast: false, }; renderWithProviders(); expect(mockChildren).toHaveBeenCalledWith( expect.objectContaining({ loadNextData: mockLoadNext, }) ); }); it("should pass correct zappPipesDataProps to children", () => { const props = { feedUrl: "https://example.com/feed", component: { ...componentRequiredKeys } as any, children: mockChildren, riverId: "test-river", }; renderWithProviders(); expect(mockChildren).toHaveBeenCalledWith({ zappPipesData: { url: "test-url", loading: false, data: { entry: [{ id: "1" }] }, error: null, }, reloadData: mockReloadData, loadNextData: mockLoadNext, }); }); it("should apply feed refresh hook", () => { const props = { feedUrl: "https://example.com/feed", component: { ...componentRequiredKeys } as any, children: mockChildren, riverId: "test-river", }; renderWithProviders(); expect(useFeedRefreshModule.useFeedRefresh).toHaveBeenCalledWith({ reloadData: mockReloadData, component: props.component, }); }); it("should clean up listeners on unmount", () => { const props = { component: { data: { source: "favorites-source", type: "FAVOURITES", }, ...componentRequiredKeys, } as any, children: mockChildren, riverId: "test-river", }; const { unmount } = renderWithProviders(); unmount(); expect(favoritesListener.removeHandler).toHaveBeenCalledWith( "FAVORITES_CHANGED", mockReloadData ); }); });