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
);
});
});